UNPKG

jwks-client

Version:

Library to retrieve public keys from a JWKS endpoint

150 lines (124 loc) 3.78 kB
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); }); } }