120 lines
4.0 KiB
JavaScript
120 lines
4.0 KiB
JavaScript
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';
|
|
case 'ML':
|
|
return 'AKP';
|
|
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' || kty === 'AKP')) {
|
|
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;
|
|
}
|