keys.js (2335B)
1 import crypto from 'node:crypto'; 2 import { 3 SignJWT, 4 exportJWK, 5 generateKeyPair, 6 importJWK, 7 jwtVerify, 8 } from 'jose'; 9 10 const pending = new WeakMap(); 11 12 async function loadSigningKey(db) { 13 const row = await db.get( 14 `SELECT id, alg, private_jwk, public_jwk 15 FROM signing_keys 16 WHERE status = 'active' 17 ORDER BY created_at DESC 18 LIMIT 1` 19 ); 20 if (row) { 21 return { 22 id: row.id, 23 alg: row.alg, 24 privateJwk: JSON.parse(row.private_jwk), 25 publicJwk: JSON.parse(row.public_jwk), 26 }; 27 } 28 29 const { privateKey, publicKey } = await generateKeyPair('ES256', { extractable: true }); 30 const id = crypto.randomUUID(); 31 const privateJwk = await exportJWK(privateKey); 32 const publicJwk = await exportJWK(publicKey); 33 34 privateJwk.kid = id; 35 privateJwk.alg = 'ES256'; 36 privateJwk.use = 'sig'; 37 publicJwk.kid = id; 38 publicJwk.alg = 'ES256'; 39 publicJwk.use = 'sig'; 40 41 await db.run( 42 `INSERT INTO signing_keys (id, alg, private_jwk, public_jwk, status, created_at) 43 VALUES ({id}, 'ES256', {privateJwk}, {publicJwk}, 'active', {createdAt})`, 44 { 45 id, 46 privateJwk: JSON.stringify(privateJwk), 47 publicJwk: JSON.stringify(publicJwk), 48 createdAt: Date.now(), 49 } 50 ); 51 52 return { id, alg: 'ES256', privateJwk, publicJwk }; 53 } 54 55 export function ensureSigningKey(db) { 56 let promise = pending.get(db); 57 if (!promise) { 58 promise = loadSigningKey(db); 59 pending.set(db, promise); 60 } 61 return promise; 62 } 63 64 export async function getJwks(db) { 65 const key = await ensureSigningKey(db); 66 return { keys: [key.publicJwk] }; 67 } 68 69 export async function signToken(db, claims, { issuer, audience, expiresIn }) { 70 const key = await ensureSigningKey(db); 71 const privateKey = await importJWK(key.privateJwk, key.alg); 72 const now = Math.floor(Date.now() / 1000); 73 74 return new SignJWT(claims) 75 .setProtectedHeader({ alg: key.alg, kid: key.id, typ: 'JWT' }) 76 .setIssuer(issuer) 77 .setAudience(audience) 78 .setIssuedAt(now) 79 .setExpirationTime(now + expiresIn) 80 .sign(privateKey); 81 } 82 83 export async function verifyToken(db, token, { issuer, audience }) { 84 const key = await ensureSigningKey(db); 85 const publicKey = await importJWK(key.publicJwk, key.alg); 86 const { payload } = await jwtVerify(token, publicKey, { issuer, audience }); 87 return payload; 88 }