import { importJWK } from '../key/import.js'; import { JWKSInvalid, JOSENotSupported, JWKSNoMatchingKey, JWKSMultipleMatchingKeys, } from '../util/errors.js'; import isObject from '../lib/is_object.js'; function getKtyFromAlg(alg) { switch (typeof alg === 'string' && alg.slice(0, 2)) { case 'RS': case 'PS': return 'RSA'; case 'ES': return 'EC'; case 'Ed': return 'OKP'; default: throw new JOSENotSupported('Unsupported "alg" value for a JSON Web Key Set'); } } function isJWKSLike(jwks) { return (jwks && typeof jwks === 'object' && Array.isArray(jwks.keys) && jwks.keys.every(isJWKLike)); } function isJWKLike(key) { return isObject(key); } class LocalJWKSet { #jwks; #cached = new WeakMap(); constructor(jwks) { if (!isJWKSLike(jwks)) { throw new JWKSInvalid('JSON Web Key Set malformed'); } this.#jwks = structuredClone(jwks); } jwks() { return this.#jwks; } async getKey(protectedHeader, token) { const { alg, kid } = { ...protectedHeader, ...token?.header }; const kty = getKtyFromAlg(alg); const candidates = this.#jwks.keys.filter((jwk) => { let candidate = kty === jwk.kty; if (candidate && typeof kid === 'string') { candidate = kid === jwk.kid; } if (candidate && typeof jwk.alg === 'string') { candidate = alg === jwk.alg; } if (candidate && typeof jwk.use === 'string') { candidate = jwk.use === 'sig'; } if (candidate && Array.isArray(jwk.key_ops)) { candidate = jwk.key_ops.includes('verify'); } if (candidate) { switch (alg) { case 'ES256': candidate = jwk.crv === 'P-256'; break; case 'ES384': candidate = jwk.crv === 'P-384'; break; case 'ES512': candidate = jwk.crv === 'P-521'; break; case 'Ed25519': case 'EdDSA': candidate = jwk.crv === 'Ed25519'; break; } } return candidate; }); const { 0: jwk, length } = candidates; if (length === 0) { throw new JWKSNoMatchingKey(); } if (length !== 1) { const error = new JWKSMultipleMatchingKeys(); const _cached = this.#cached; error[Symbol.asyncIterator] = async function* () { for (const jwk of candidates) { try { yield await importWithAlgCache(_cached, jwk, alg); } catch { } } }; throw error; } return importWithAlgCache(this.#cached, jwk, alg); } } async function importWithAlgCache(cache, jwk, alg) { const cached = cache.get(jwk) || cache.set(jwk, {}).get(jwk); if (cached[alg] === undefined) { const key = await importJWK({ ...jwk, ext: true }, alg); if (key instanceof Uint8Array || key.type !== 'public') { throw new JWKSInvalid('JSON Web Key Set members must be public keys'); } cached[alg] = key; } return cached[alg]; } export function createLocalJWKSet(jwks) { const set = new LocalJWKSet(jwks); const localJWKSet = async (protectedHeader, token) => set.getKey(protectedHeader, token); Object.defineProperties(localJWKSet, { jwks: { value: () => structuredClone(set.jwks()), enumerable: false, configurable: false, writable: false, }, }); return localJWKSet; }