119 lines
4.2 KiB
JavaScript
119 lines
4.2 KiB
JavaScript
|
'use strict';
|
||
|
|
||
|
const Jwk = require('./Jwk.js');
|
||
|
const debug = require('debug');
|
||
|
const debugTrace = debug('xssec:JwksReplica');
|
||
|
const debugError = debug('xssec:JwksReplica');
|
||
|
debugTrace.log = console.log.bind(console);
|
||
|
debugError.log = console.error.bind(console);
|
||
|
|
||
|
class JwksReplica {
|
||
|
static get DEFAULT_EXPIRATION_TIME() { return 30 * 60 * 1000; } // 30 minutes
|
||
|
static get DEFAULT_REFRESH_PERIOD() { return 15 * 60 * 1000; } // 15 minutes
|
||
|
|
||
|
#oAuth2Service; // OAuth2 service from the JWKS is fetched
|
||
|
#params; // params to use when retrieving the JWKS
|
||
|
#keys; // map that holds the JWKS mapped by kid
|
||
|
#jwksUpdate; // promise for ongoing update of JWKS or undefined
|
||
|
#expirationTime; // default expiration time that will be used for new JWK cache entries
|
||
|
#refreshPeriod; // time before expiration in which a JWT is considered stale
|
||
|
|
||
|
get params() {return this.#params;}
|
||
|
|
||
|
constructor(oAuth2Service, expirationTime = JwksReplica.DEFAULT_EXPIRATION_TIME, refreshPeriod = JwksReplica.DEFAULT_REFRESH_PERIOD) {
|
||
|
if(!oAuth2Service) {
|
||
|
throw new Error("JwksReplica requires an oAuth2Service to fetch the JWKS from.")
|
||
|
}
|
||
|
|
||
|
if (expirationTime < 0) {
|
||
|
throw new Error("JwksReplica expirationTime must be >=0.")
|
||
|
}
|
||
|
|
||
|
if (refreshPeriod < 0 || refreshPeriod > expirationTime) {
|
||
|
throw new Error("JwksReplica refreshPeriod must be between 0 and <expirationTime>.")
|
||
|
}
|
||
|
|
||
|
this.#oAuth2Service = oAuth2Service;
|
||
|
this.#keys = new Map();
|
||
|
this.#expirationTime = expirationTime;
|
||
|
this.#refreshPeriod = refreshPeriod;
|
||
|
}
|
||
|
|
||
|
get oAuth2Service() { return this.#oAuth2Service; }
|
||
|
get expirationTime() { return this.#expirationTime; }
|
||
|
get refreshPeriod() { return this.#refreshPeriod; }
|
||
|
|
||
|
/** Configures the replica with parameters that are used when retrieving the JWKS from the server. */
|
||
|
withParams(params) {
|
||
|
this.#params = params;
|
||
|
return this;
|
||
|
}
|
||
|
|
||
|
put(kid, value, expirationTime = this.expirationTime) {
|
||
|
const jwk = new Jwk(kid, value, expirationTime);
|
||
|
this.#keys.set(kid, jwk);
|
||
|
return jwk;
|
||
|
}
|
||
|
|
||
|
async get(kid) {
|
||
|
let jwk = this.#keys.get(kid);
|
||
|
|
||
|
if (jwk === undefined || jwk.expired) {
|
||
|
debugTrace(`Awaiting JWKS refresh because JWK with kid=${kid} is ${jwk === undefined ? "missing" : "expired"}. OAuth2Service: (${JSON.stringify(this.#oAuth2Service)})`);
|
||
|
await this.updateJwks();
|
||
|
jwk = this.#keys.get(kid);
|
||
|
|
||
|
if (jwk === undefined) {
|
||
|
const err = new Error(`JWKS does not contain JWK with kid ${kid}.`);
|
||
|
err.kidMissingInJwks = true;
|
||
|
throw err;
|
||
|
}
|
||
|
|
||
|
return jwk;
|
||
|
}
|
||
|
|
||
|
// trigger asynchronous JWKS update if this JWK is stale
|
||
|
if (jwk.stale(this.#refreshPeriod)) {
|
||
|
if(!this.#jwksUpdate) {
|
||
|
debugTrace(`Asynchronous JWKS refresh scheduled because JWK with kid=${kid} is stale (remaining time = ${jwk.remainingTime}ms < ${this.refreshPeriod}ms = refresh period). OAuth2Service: (${JSON.stringify(this.#oAuth2Service)})`);
|
||
|
}
|
||
|
|
||
|
this.updateJwks().catch((e) => {
|
||
|
debugError("Asynchronous JWKS refresh failed.", e)
|
||
|
});
|
||
|
}
|
||
|
|
||
|
return jwk;
|
||
|
}
|
||
|
|
||
|
async updateJwks() {
|
||
|
if (this.#jwksUpdate === undefined) {
|
||
|
this.#jwksUpdate = this.#doUpdateJwks();
|
||
|
}
|
||
|
|
||
|
return this.#jwksUpdate;
|
||
|
}
|
||
|
|
||
|
async #doUpdateJwks() {
|
||
|
debugTrace(`Fetching JWKS from service ${JSON.stringify(this.#oAuth2Service)}.`)
|
||
|
try {
|
||
|
const jwks = await this.#oAuth2Service.fetchJwks(this.params);
|
||
|
debugTrace(`JWKS received from service ${JSON.stringify(this.#oAuth2Service)}.`)
|
||
|
|
||
|
this.#keys.clear();
|
||
|
for (let key of jwks) {
|
||
|
this.put(key.kid, key.value);
|
||
|
}
|
||
|
|
||
|
return jwks;
|
||
|
} catch(e) {
|
||
|
debugError(`Error fetching JWKS from service ${JSON.stringify(this.#oAuth2Service)}.`, e);
|
||
|
throw e;
|
||
|
} finally {
|
||
|
this.#jwksUpdate = undefined;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
module.exports = JwksReplica;
|