idp-signing.ts75 lines · main
| 1 | /** |
| 2 | * RSA signing keys for OIDC id_token / access_token (RS256 + JWKS). |
| 3 | * First active key is created on demand and stored in Doltgres. |
| 4 | */ |
| 5 | |
| 6 | import { generateKeyPair, exportJWK, exportPKCS8, importPKCS8, type JWK, type KeyLike } from 'jose'; |
| 7 | import { randomBytes } from 'node:crypto'; |
| 8 | |
| 9 | import { getEnginePool } from './db.js'; |
| 10 | import { log } from '../../lib/logger.js'; |
| 11 | |
| 12 | type CachedKey = { |
| 13 | kid: string; |
| 14 | privateKey: KeyLike; |
| 15 | publicJwk: JWK; |
| 16 | }; |
| 17 | |
| 18 | let cache: CachedKey | null = null; |
| 19 | |
| 20 | function newKid(): string { |
| 21 | return `oidc_${randomBytes(8).toString('hex')}`; |
| 22 | } |
| 23 | |
| 24 | export async function ensureOidcSigningKey(): Promise<CachedKey> { |
| 25 | if (cache) return cache; |
| 26 | const pool = getEnginePool(); |
| 27 | const existing = await pool.query( |
| 28 | `SELECT kid, private_pem, public_jwk_json FROM be_oidc_signing_keys |
| 29 | WHERE active = TRUE ORDER BY created_at DESC LIMIT 1`, |
| 30 | ); |
| 31 | const row = existing.rows[0] as |
| 32 | | { kid: string; private_pem: string; public_jwk_json: string } |
| 33 | | undefined; |
| 34 | |
| 35 | if (row) { |
| 36 | const privateKey = await importPKCS8(row.private_pem, 'RS256'); |
| 37 | const publicJwk = JSON.parse(row.public_jwk_json) as JWK; |
| 38 | cache = { kid: row.kid, privateKey, publicJwk }; |
| 39 | return cache; |
| 40 | } |
| 41 | |
| 42 | const { privateKey, publicKey } = await generateKeyPair('RS256', { |
| 43 | extractable: true, |
| 44 | }); |
| 45 | const kid = newKid(); |
| 46 | const privatePem = await exportPKCS8(privateKey); |
| 47 | const publicJwk = await exportJWK(publicKey); |
| 48 | publicJwk.kid = kid; |
| 49 | publicJwk.use = 'sig'; |
| 50 | publicJwk.alg = 'RS256'; |
| 51 | |
| 52 | await pool.query( |
| 53 | `INSERT INTO be_oidc_signing_keys (kid, private_pem, public_jwk_json, active, created_at) |
| 54 | VALUES ($1, $2, $3, TRUE, NOW())`, |
| 55 | [kid, privatePem, JSON.stringify(publicJwk)], |
| 56 | ); |
| 57 | log.info('oidc_signing_key_created', { kid, engine: 'briven-engine' }); |
| 58 | cache = { kid, privateKey, publicJwk }; |
| 59 | return cache; |
| 60 | } |
| 61 | |
| 62 | export async function getOidcJwks(): Promise<{ keys: JWK[] }> { |
| 63 | const pool = getEnginePool(); |
| 64 | const res = await pool.query( |
| 65 | `SELECT public_jwk_json FROM be_oidc_signing_keys WHERE active = TRUE ORDER BY created_at DESC`, |
| 66 | ); |
| 67 | if (res.rows.length === 0) { |
| 68 | const k = await ensureOidcSigningKey(); |
| 69 | return { keys: [k.publicJwk] }; |
| 70 | } |
| 71 | const keys = (res.rows as Array<{ public_jwk_json: string }>).map( |
| 72 | (r) => JSON.parse(r.public_jwk_json) as JWK, |
| 73 | ); |
| 74 | return { keys }; |
| 75 | } |