buildmymcpserver/apps/api/src/lib/jwks.ts

74 lines
2.4 KiB
TypeScript

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<KeyMaterial> {
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<string> {
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);
}