import fs from 'node:fs'; import path from 'node:path'; import crypto from 'node:crypto'; import { exportJWK, importPKCS8, importSPKI, type JWK, type KeyLike, SignJWT } from 'jose'; import { config } from '../config.js'; interface KeyMaterial { kid: string; privatePem: string; publicPem: string; privateKey: KeyLike; publicJwk: JWK; } let cached: KeyMaterial | null = null; async function loadOrGenerate(): Promise { if (cached) return cached; const dir = path.resolve(config.OAUTH_KEY_DIR); fs.mkdirSync(dir, { recursive: true }); const privPath = path.join(dir, 'oauth-private.pem'); const pubPath = path.join(dir, 'oauth-public.pem'); const kidPath = path.join(dir, 'oauth.kid'); let privatePem: string; let publicPem: string; let kid: string; if (fs.existsSync(privPath) && fs.existsSync(pubPath) && fs.existsSync(kidPath)) { privatePem = fs.readFileSync(privPath, 'utf8'); publicPem = fs.readFileSync(pubPath, 'utf8'); kid = fs.readFileSync(kidPath, 'utf8').trim(); } else { const { publicKey, privateKey } = crypto.generateKeyPairSync('rsa', { modulusLength: 2048 }); privatePem = privateKey.export({ type: 'pkcs8', format: 'pem' }) as string; publicPem = publicKey.export({ type: 'spki', format: 'pem' }) as string; kid = crypto.randomBytes(8).toString('hex'); fs.writeFileSync(privPath, privatePem, { mode: 0o600 }); fs.writeFileSync(pubPath, publicPem, { mode: 0o644 }); fs.writeFileSync(kidPath, kid); } const privateKey = await importPKCS8(privatePem, 'RS256'); const publicKey = await importSPKI(publicPem, 'RS256'); const publicJwk: JWK = { ...(await exportJWK(publicKey)), kid, alg: 'RS256', use: 'sig' }; cached = { kid, privatePem, publicPem, privateKey, publicJwk }; return cached; } export async function getJWKS(): Promise<{ keys: JWK[] }> { const k = await loadOrGenerate(); return { keys: [k.publicJwk] }; } export async function signAccessToken(input: { subject: string; audience: string; issuer: string; scope?: string; ttlSeconds?: number; }): Promise { const k = await loadOrGenerate(); const ttl = input.ttlSeconds ?? 3600; return await new SignJWT({ scope: input.scope ?? '' }) .setProtectedHeader({ alg: 'RS256', kid: k.kid, typ: 'JWT' }) .setIssuer(input.issuer) .setSubject(input.subject) .setAudience(input.audience) .setIssuedAt() .setExpirationTime(`${ttl}s`) .sign(k.privateKey); }