|
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; |
|
} |
|
|