jwks-client
Version:
Library to retrieve public keys from a JWKS endpoint
150 lines (124 loc) • 3.78 kB
JavaScript
import debug from 'debug';
import fetch from 'node-fetch';
import * as jose from 'jose'
import JwksError from './lib/errors/JwksError.js';
import SigningKeyNotFoundError from './lib/errors/SigningKeyNotFoundError.js';
import rateLimitSigningKey from './lib/wrappers/rateLimit.js';
import cacheSigningKey from './lib/wrappers/cache.js';
const logger = debug('jwks');
export default class JwksClient {
constructor(options) {
this.options = Object.assign({ rateLimit: false, cache: false, headers: {} }, options);
let _this = this;
this.getSigningKey = function(kid, cb) {
logger(`Fetching signing key for '${kid}'`);
_this.getSigningKeys((err, keys) => {
if (err) {
return cb(err);
}
const key = keys.find(k => k.kid === kid);
if (key) {
return cb(null, key);
} else {
logger(`Unable to find a signing key that matches '${kid}'`);
return cb(new SigningKeyNotFoundError(`Unable to find a signing key that matches '${kid}'`));
}
});
};
// Initialize wrappers.
if (this.options.rateLimit) {
this.getSigningKey = rateLimitSigningKey(this, options);
}
if (this.options.cache) {
this.getSigningKey = cacheSigningKey(this, options);
}
}
async getKeys(cb) {
logger(`Fetching keys from '${this.options.jwksUri}'`);
try {
const response = await fetch(this.options.jwksUri, {
headers: this.options.headers
});
let data = await response.text();
if(!response.ok) {
logger('Failure:', response);
return cb(new JwksError(data.message || data));
} else {
data = JSON.parse(data);
}
logger('Keys:', data.keys);
return cb(null, data.keys);
} catch( error ) {
cb(error);
}
}
resolveAlg(jwk) {
if (jwk.alg) {
return jwk.alg;
}
if (jwk.kty === 'RSA') {
return 'RS256';
}
if (jwk.kty === 'EC') {
switch (jwk.crv) {
case 'P-256':
return 'ES256';
case 'P-384':
return 'ES384';
case 'P-521':
return 'ES512';
}
}
if (jwk.kty === 'OKP') {
switch (jwk.crv) {
case 'Ed25519':
case 'Ed448':
return 'EdDSA';
}
}
return null;
}
getSigningKeys(cb) {
this.getKeys(async (err, keys) => {
if (err) {
return cb(err);
}
if (!keys || !keys.length) {
return cb(new JwksError('The JWKS endpoint did not contain any keys'));
}
const signingKeys = [];
keys = keys.filter(key => {
return key.use === 'sig' && key.kid;
});
for (const key of keys) {
logger(`Processing ${key.kid}`);
const algorithm = this.resolveAlg(key);
logger(`Algorithm set as: ${algorithm}`);
if(!algorithm) {
logger('Algorithm was not supplied and could not make a valid guess, skipping');
continue;
}
if(key.kty && key.kty === "EC" && key.d) {
logger(`Mixed container supplied, this also contains the private key!`);
delete key.d;
}
const importedKey = await jose.importJWK(key, algorithm);
if (importedKey.type === 'public') {
logger('Key imported:', importedKey);
const publicKey = await jose.exportSPKI(importedKey);
let result = {
kid: key.kid,
nbf: key.nbf,
publicKey: publicKey
};
signingKeys.push(result);
}
}
if (!signingKeys.length) {
return cb(new JwksError('The JWKS endpoint did not contain any signing keys'));
}
logger('Signing Keys:', signingKeys);
return cb(null, signingKeys);
});
}
}