SAP-BTP-Spielwiese/app1/node_modules/@sap/xssec/lib/jwks/JwksReplica.js

119 lines
4.2 KiB
JavaScript
Raw Normal View History

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