import { SignJWT, exportJWK, generateKeyPair } from 'jose'; import type { GenerateKeyPairResult } from 'jose'; export interface TestIdentity { sub?: string; username?: string; roles?: string[]; groups?: string[]; azp?: string; aud?: string | string[]; expiresIn?: number; // 秒;负数=已过期 } let keys: GenerateKeyPairResult | null = null; /** 生成本地密钥对并写入 KEYCLOAK_TEST_JWKS(必须先于首次 verify 调用) */ export async function setupTestJwks(): Promise { if (keys) return; keys = await generateKeyPair('RS256'); const jwk = await exportJWK(keys.publicKey); process.env.KEYCLOAK_TEST_JWKS = JSON.stringify({ keys: [{ ...jwk, alg: 'RS256', kid: 'test-key' }] }); } export async function makeToken(id: TestIdentity = {}): Promise { await setupTestJwks(); const issuer = `${process.env.KEYCLOAK_URL}/realms/${process.env.KEYCLOAK_REALM}`; const clientId = process.env.KEYCLOAK_CLIENT_ID ?? ''; if (!keys) throw new Error('test keys not ready'); return new SignJWT({ preferred_username: id.username ?? 'tester', realm_access: { roles: id.roles ?? ['user'] }, groups: id.groups ?? [], azp: id.azp ?? 'web-portal', }) .setProtectedHeader({ alg: 'RS256', kid: 'test-key' }) .setSubject(id.sub ?? 'test-sub-1') .setIssuer(issuer) .setAudience(id.aud ?? clientId) .setIssuedAt() .setExpirationTime(Math.floor(Date.now() / 1000) + (id.expiresIn ?? 300)) .sign(keys.privateKey); }