jose
Version:
Universal 'JSON Web Almost Everything' - JWA, JWS, JWE, JWT, JWK with no dependencies
128 lines (127 loc) • 5.03 kB
JavaScript
import parseJWK from '../jwk/parse.js';
import { JWKSInvalid, JOSENotSupported, JWKSNoMatchingKey, JWKSMultipleMatchingKeys, } from '../util/errors.js';
import fetchJson from '../runtime/fetch.js';
function getKtyFromAlg(alg) {
switch (alg.substr(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');
}
}
class RemoteJWKSet {
constructor(url, options) {
this._cached = new WeakMap();
if (!(url instanceof URL)) {
throw new TypeError('url must be an instance of URL');
}
this._url = new URL(url.href);
this._options = { agent: options === null || options === void 0 ? void 0 : options.agent };
this._timeoutDuration =
typeof (options === null || options === void 0 ? void 0 : options.timeoutDuration) === 'number' ? options === null || options === void 0 ? void 0 : options.timeoutDuration : 5000;
this._cooldownDuration =
typeof (options === null || options === void 0 ? void 0 : options.cooldownDuration) === 'number' ? options === null || options === void 0 ? void 0 : options.cooldownDuration : 30000;
}
coolingDown() {
if (typeof this._cooldownStarted === 'undefined') {
return false;
}
return Date.now() < this._cooldownStarted + this._cooldownDuration;
}
async getKey(protectedHeader) {
if (!this._jwks) {
await this.reload();
}
const candidates = this._jwks.keys.filter((jwk) => {
let candidate = jwk.kty === getKtyFromAlg(protectedHeader.alg);
if (candidate && typeof protectedHeader.kid === 'string') {
candidate = protectedHeader.kid === jwk.kid;
}
if (candidate && typeof jwk.alg === 'string') {
candidate = protectedHeader.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 && protectedHeader.alg === 'EdDSA') {
candidate = ['Ed25519', 'Ed448'].includes(jwk.crv);
}
if (candidate) {
switch (protectedHeader.alg) {
case 'ES256':
candidate = jwk.crv === 'P-256';
break;
case 'ES256K':
candidate = jwk.crv === 'secp256k1';
break;
case 'ES384':
candidate = jwk.crv === 'P-384';
break;
case 'ES512':
candidate = jwk.crv === 'P-521';
break;
default:
}
}
return candidate;
});
const { 0: jwk, length } = candidates;
if (length === 0) {
if (this.coolingDown() === false) {
await this.reload();
return this.getKey(protectedHeader);
}
throw new JWKSNoMatchingKey();
}
else if (length !== 1) {
throw new JWKSMultipleMatchingKeys();
}
if (!this._cached.has(jwk)) {
this._cached.set(jwk, {});
}
const cached = this._cached.get(jwk);
if (cached[protectedHeader.alg] === undefined) {
const keyObject = (await parseJWK({ ...jwk, alg: protectedHeader.alg }));
if (keyObject.type !== 'public') {
throw new JWKSInvalid('JSON Web Key Set members must be public keys');
}
cached[protectedHeader.alg] = keyObject;
}
return cached[protectedHeader.alg];
}
async reload() {
if (!this._pendingFetch) {
this._pendingFetch = fetchJson(this._url, this._timeoutDuration, this._options)
.then((json) => {
if (typeof json !== 'object' ||
!json ||
!Array.isArray(json.keys) ||
json.keys.some((key) => typeof key !== 'object' || !key)) {
throw new JWKSInvalid('JSON Web Key Set malformed');
}
this._jwks = json;
this._cooldownStarted = Date.now();
this._pendingFetch = undefined;
})
.catch((err) => {
this._pendingFetch = undefined;
throw err;
});
}
await this._pendingFetch;
}
}
function createRemoteJWKSet(url, options) {
const set = new RemoteJWKSet(url, options);
return set.getKey.bind(set);
}
export { createRemoteJWKSet };
export default createRemoteJWKSet;