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
6import { generateKeyPair, exportJWK, exportPKCS8, importPKCS8, type JWK, type KeyLike } from 'jose';
7import { randomBytes } from 'node:crypto';
8
9import { getEnginePool } from './db.js';
10import { log } from '../../lib/logger.js';
11
12type CachedKey = {
13 kid: string;
14 privateKey: KeyLike;
15 publicJwk: JWK;
16};
17
18let cache: CachedKey | null = null;
19
20function newKid(): string {
21 return `oidc_${randomBytes(8).toString('hex')}`;
22}
23
24export 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
62export 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}