jwks-rsa
Version:
Library to retrieve RSA public keys from a JWKS endpoint
57 lines (51 loc) • 2.31 kB
JavaScript
const logger = require('debug')('jwks');
const memoizer = require('lru-memoizer');
const { LRUCache } = require('lru-cache');
const { promisify, callbackify } = require('util');
function cacheWrapper(client, { cacheMaxEntries = 5, cacheMaxAge = 600000, cacheMaxAgeFallback, onStaleCacheFallback }) {
logger(`Configured caching of signing keys. Max: ${cacheMaxEntries} / Age: ${cacheMaxAge}${cacheMaxAgeFallback ? ` / Fallback: ${cacheMaxAgeFallback}` : ''}`);
/**
* cacheMaxAgeFallback: when the JWKS endpoint is unreachable and cacheMaxAge has expired,
* rather than immediately failing to return a signing key, the lib can continue serving
* the last known good key for an additional window (cacheMaxAgeFallback ms). This prevents
* callers from failing on every getSigningKey call during a transient JWKS endpoint downtime.
*
* Two load variants: with cacheMaxAgeFallback, maintain a stale-key store so a failed
* refresh can fall back to the last known good key within the window. Without it, skip
* the extra cache entirely to avoid any overhead.
*/
let load;
if (cacheMaxAgeFallback) {
const staleCache = new LRUCache({ max: cacheMaxEntries });
const getSigningKey = client.getSigningKey.bind(client);
load = callbackify(async (kid) => {
try {
const key = await getSigningKey(kid);
staleCache.set(kid, { key, fetchedAt: Date.now() });
return key;
} catch (err) {
if (err.isEndpointUnavailable) {
const stale = staleCache.get(kid);
if (stale && (Date.now() - stale.fetchedAt) < (cacheMaxAge + cacheMaxAgeFallback)) {
logger(`JWKS endpoint unavailable, serving stale signing key for '${kid}': ${err.message}`);
if (onStaleCacheFallback) {
onStaleCacheFallback(err, kid, stale.key);
}
return stale.key;
}
logger(`JWKS endpoint unavailable and no valid stale entry for '${kid}', fallback window expired or key never fetched`);
}
throw err;
}
});
} else {
load = callbackify(client.getSigningKey.bind(client));
}
return promisify(memoizer({
hash: (kid) => kid,
load,
ttl: cacheMaxAge,
max: cacheMaxEntries
}));
}
module.exports.default = cacheWrapper;