import { JwtPayloadWithClaims } from "./jwt.ts"; import { importJWK, importJWKFromString } from "./keys.ts"; export interface JwksIssuer { verifyWith: < TClaims extends Record = Record, >( cb: (key: CryptoKey) => Promise>, ) => Promise>; } export interface JwksIssuerOptions { fallbackPublicKey?: string; remoteAddress: string; kid?: string; } export interface JwksKeys { keys: Array; } const fetchKeyFromAddr = async ( address: string, kid?: string, ): Promise => { const response = await fetch(address); if (!response.ok) { return null; } const { keys = [] } = await response.json(); const key = kid ? keys.find((key) => key?.kid === kid) ?? keys[0] : keys[0]; if (!key) { return null; } return importJWK(key, ["verify"]); }; export const newJwksIssuer = ( { fallbackPublicKey, remoteAddress, kid }: JwksIssuerOptions, ): JwksIssuer => { let fallbackKey: Promise | null = null; let currentKey: Promise | null = null; return { verifyWith: async (cb) => { currentKey ??= fetchKeyFromAddr(remoteAddress, kid); const key = await currentKey.then(async (c) => { if (c === null && fallbackPublicKey) { fallbackKey ??= importJWKFromString(fallbackPublicKey, ["verify"]); return fallbackKey; } return c; }); if (key === null) { throw new Error("none of the provided keys are available"); } try { return cb(key); } catch (err) { // if an error was thrown maybe the key was rotated so we should refetch it const currKey = await currentKey; if (currKey !== null) { // means that the has been used currentKey = fetchKeyFromAddr(remoteAddress, kid); const newKey = await currentKey; if (newKey) { return cb(newKey); } } throw err; } }, }; };