diff --git a/deploy/docker-compose.yml b/deploy/docker-compose.yml index a435826b8..b1daebbf8 100644 --- a/deploy/docker-compose.yml +++ b/deploy/docker-compose.yml @@ -60,6 +60,12 @@ services: AUTH_DB_URL: ${AUTH_DB_URL} AUTH_DB_USERNAME: ${AUTH_DB_USERNAME} AUTH_DB_PASSWORD: ${AUTH_DB_PASSWORD} + OVERSEAS_JWT_ENABLED: ${OVERSEAS_JWT_ENABLED:-false} + OVERSEAS_JWT_ISSUER: ${OVERSEAS_JWT_ISSUER:-auth-service} + OVERSEAS_JWT_AUDIENCE: ${OVERSEAS_JWT_AUDIENCE:-lastleg-api} + OVERSEAS_JWT_RSA_JWK_SET: ${OVERSEAS_JWT_RSA_JWK_SET:-} + OVERSEAS_JWT_CURRENT_KID: ${OVERSEAS_JWT_CURRENT_KID:-} + OVERSEAS_ACCESS_TOKEN_TTL: ${OVERSEAS_ACCESS_TOKEN_TTL:-15m} iam: <<: *common diff --git a/scripts/generate-overseas-jwk.mjs b/scripts/generate-overseas-jwk.mjs new file mode 100644 index 000000000..c80f7f678 --- /dev/null +++ b/scripts/generate-overseas-jwk.mjs @@ -0,0 +1,36 @@ +import { generateKeyPairSync, randomUUID } from 'node:crypto' +import { readFileSync, writeFileSync } from 'node:fs' +import { basename, dirname, resolve } from 'node:path' + +const envPath = resolve(process.argv[2] || '') +if (!envPath || basename(dirname(envPath)) !== 'env' || !basename(envPath).startsWith('.env.')) { + throw new Error('用法: node scripts/generate-overseas-jwk.mjs deploy/env/.env.') +} + +let source = readFileSync(envPath, 'utf8') +const existing = source.match(/^OVERSEAS_JWT_RSA_JWK_SET=(.*)$/m)?.[1]?.trim() +if (existing) throw new Error('目标环境已经配置 OVERSEAS_JWT_RSA_JWK_SET,拒绝覆盖') + +const environment = basename(envPath).slice('.env.'.length).replace(/[^a-zA-Z0-9-]/g, '-') +const kid = `${environment}-overseas-${new Date().toISOString().slice(0, 10)}-${randomUUID().slice(0, 8)}` +const { privateKey } = generateKeyPairSync('rsa', { modulusLength: 3072 }) +const privateJwk = privateKey.export({ format: 'jwk' }) +const jwkSet = JSON.stringify({ keys: [{ ...privateJwk, use: 'sig', alg: 'RS256', kid }] }) + +const values = new Map([ + ['OVERSEAS_JWT_ENABLED', 'true'], + ['OVERSEAS_JWT_ISSUER', 'auth-service'], + ['OVERSEAS_JWT_AUDIENCE', 'lastleg-api'], + ['OVERSEAS_JWT_RSA_JWK_SET', `'${jwkSet}'`], + ['OVERSEAS_JWT_CURRENT_KID', kid], + ['OVERSEAS_ACCESS_TOKEN_TTL', '15m'], +]) + +for (const [key, value] of values) { + const pattern = new RegExp(`^${key}=.*$`, 'm') + if (pattern.test(source)) source = source.replace(pattern, `${key}=${value}`) + else source += `${source.endsWith('\n') ? '' : '\n'}${key}=${value}\n` +} + +writeFileSync(envPath, source, 'utf8') +process.stdout.write(`Configured development-only overseas JWK kid: ${kid}\n`) diff --git a/scripts/overseas-deployment-contract.test.mjs b/scripts/overseas-deployment-contract.test.mjs index 1c1f3b10f..38250cf7d 100644 --- a/scripts/overseas-deployment-contract.test.mjs +++ b/scripts/overseas-deployment-contract.test.mjs @@ -67,3 +67,11 @@ test('CN and overseas event bridges have explicit opposite-region consumers', as 'JWT_ISSUER', 'OVERSEAS_JWT_AUDIENCE', 'JWT_JWK_SET_URI' ]) assert.match(scripts, new RegExp(key)) }) + +test('domestic Auth receives the overseas JWT signing configuration', async () => { + const compose = await read('deploy/docker-compose.yml') + for (const key of [ + 'OVERSEAS_JWT_ENABLED', 'OVERSEAS_JWT_ISSUER', 'OVERSEAS_JWT_AUDIENCE', + 'OVERSEAS_JWT_RSA_JWK_SET', 'OVERSEAS_JWT_CURRENT_KID', 'OVERSEAS_ACCESS_TOKEN_TTL' + ]) assert.match(compose, new RegExp(`${key}: \\$\\{${key}`)) +})