Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Simplify request wrapper #218

Merged
merged 5 commits into from
Feb 24, 2021
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Migrate from callbacks to async/await
  • Loading branch information
davidpatrick committed Feb 23, 2021
commit 8c34eeeaf9f7285ba23169003dee6e4cfac45032
25 changes: 8 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,8 @@ const client = jwksClient({
});

const kid = 'RkI5MjI5OUY5ODc1N0Q4QzM0OUYzNkVGMTJDOUEzQkFCOTU3NjE2Rg';
client.getSigningKey(kid, (err, key) => {
const signingKey = key.getPublicKey();

// Now I can use this to configure my Express or Hapi middleware
});
const key = await client.getSigningKey(kid);
const signingKey = key.getPublicKey();
```

> Note that all methods on the `JwksClient` have asynchronous equivalents, where the promisified name is suffixed with `Async`, e.g., `client.getSigningKeyAsync(kid).then(key => { /* ... */ })`;
Expand All @@ -57,11 +54,8 @@ const client = jwksClient({
});

const kid = 'RkI5MjI5OUY5ODc1N0Q4QzM0OUYzNkVGMTJDOUEzQkFCOTU3NjE2Rg';
client.getSigningKey(kid, (err, key) => {
const signingKey = key.getPublicKey();

// Now I can use this to configure my Express or Hapi middleware
});
const key = await client.getSigningKey(kid);
const signingKey = key.getPublicKey();
```

### Rate Limiting
Expand All @@ -78,11 +72,8 @@ const client = jwksClient({
});

const kid = 'RkI5MjI5OUY5ODc1N0Q4QzM0OUYzNkVGMTJDOUEzQkFCOTU3NjE2Rg';
client.getSigningKey(kid, (err, key) => {
const signingKey = key.getPublicKey();

// Now I can use this to configure my Express or Hapi middleware
});
const key = await client.getSigningKey(kid);
const signingKey = key.getPublicKey();
```

### Using Request Agent for TLS/SSL Configuration
Expand Down Expand Up @@ -115,9 +106,9 @@ The `getKeysInterceptor` property can be used to fetch keys before sending a req
```js
const client = new JwksClient({
jwksUri: 'https://my-enterprise-id-provider/.well-known/jwks.json',
getKeysInterceptor: (cb) => {
getKeysInterceptor: () => {
const file = fs.readFileSync(jwksFile);
return cb(null, file.keys);
return file.keys;
}
});
```
Expand Down
7 changes: 4 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"babel-cli": "^6.9.0",
"babel-core": "^6.9.0",
"babel-eslint": "^8.2.6",
"babel-polyfill": "^6.24.1",
"babel-preset-es2015": "^6.9.0",
"babel-preset-stage-0": "^6.5.0",
"chai": "^3.5.0",
Expand All @@ -48,11 +49,11 @@
"compile": "babel -d lib/ src/",
"lint": "eslint ./src ./tests",
"prepublish": "npm run clean && npm run compile",
"test:ts": "npm run clean:ts && tsc && NODE_ENV=test mocha --require babel-core/register --exit --timeout 5000 $(find ./ts-output -name *.tests.js)",
"test:js": "NODE_ENV=test mocha --require babel-core/register --exit --timeout 5000 $(find ./tests -name *.tests.js)",
"test:ts": "npm run clean:ts && tsc && NODE_ENV=test mocha --require babel-core/register --require babel-polyfill --exit --timeout 5000 $(find ./ts-output -name *.tests.js)",
"test:js": "NODE_ENV=test mocha --require babel-core/register --require babel-polyfill --exit --timeout 5000 $(find ./tests -name *.tests.js)",
"test": "npm run test:js && npm run test:ts",
"test:ci": "nyc --reporter=lcov npm test",
"test-watch": "NODE_ENV=test mocha --require babel-core/register --exit --timeout 5000 $(find ./tests -name *.tests.js) --watch",
"test-watch": "NODE_ENV=test mocha --require babel-core/register --require babel-polyfill --exit --timeout 5000 $(find ./tests -name *.tests.js) --watch",
"release": "git tag $npm_package_version && git push && git push --tags && npm publish"
},
"repository": {
Expand Down
165 changes: 43 additions & 122 deletions src/JwksClient.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,141 +34,62 @@ export class JwksClient {
if (this.options.cache) {
this.getSigningKey = cacheSigningKey(this, options);
}

if (this.options.rateLimit || this.options.cache) {
this.getSigningKeyAsync = promisifyIt(this.getSigningKey, this);
}
}

getKeys(cb) {
async getKeys() {
this.logger(`Fetching keys from '${this.options.jwksUri}'`);
request({
uri: this.options.jwksUri,
headers: this.options.requestHeaders,
agent: this.options.requestAgent,
timeout: this.options.timeout,
fetcher: this.options.fetcher
}).then((res) => {
this.logger('Keys:', res.keys);
return cb(null, res.keys);
}).catch((err) => {

try {
const res = await request({
uri: this.options.jwksUri,
headers: this.options.requestHeaders,
agent: this.options.requestAgent,
timeout: this.options.timeout,
fetcher: this.options.fetcher
});

this.logger('Keys:', res.keys);
return res.keys;
} catch (err) {
const { errorMsg } = err;
this.logger('Failure:', errorMsg || err);
return cb(errorMsg ? new JwksError(errorMsg) : err);
});
throw (errorMsg ? new JwksError(errorMsg) : err);
}
}

getSigningKeys(cb) {
this.getKeys((err, keys) => {
if (err) {
return cb(err);
}
async getSigningKeys() {
const keys = await this.getKeys();

if (!keys || !keys.length) {
return cb(new JwksError('The JWKS endpoint did not contain any keys'));
}
if (!keys || !keys.length) {
throw new JwksError('The JWKS endpoint did not contain any keys');
}

const signingKeys = retrieveSigningKeys(keys);
const signingKeys = retrieveSigningKeys(keys);

if (!signingKeys.length) {
return cb(new JwksError('The JWKS endpoint did not contain any signing keys'));
}
if (!signingKeys.length) {
throw new JwksError('The JWKS endpoint did not contain any signing keys');
}

this.logger('Signing Keys:', signingKeys);
return cb(null, signingKeys);
});
this.logger('Signing Keys:', signingKeys);
return signingKeys;
}

getSigningKey = (kid, cb) => {
async getSigningKey (kid) {
this.logger(`Fetching signing key for '${kid}'`);
this.getSigningKeys((err, keys) => {
if (err) {
return cb(err);
}

const kidDefined = kid !== undefined && kid !== null;
if (!kidDefined && keys.length > 1) {
this.logger('No KID specified and JWKS endpoint returned more than 1 key');
return cb(new SigningKeyNotFoundError('No KID specified and JWKS endpoint returned more than 1 key'));
}

const key = keys.find(k => !kidDefined || k.kid === kid);
if (key) {
return cb(null, key);
} else {
this.logger(`Unable to find a signing key that matches '${kid}'`);
return cb(new SigningKeyNotFoundError(`Unable to find a signing key that matches '${kid}'`));
}
});
}
const keys = await this.getSigningKeys();

/**
* Get all keys. Use this if you prefer to use Promises or async/await.
*
* @example
* client.getKeysAsync()
* .then(keys => { console.log(`Returned {keys.length} keys`); })
* .catch(err => { console.error('Error getting keys', err); });
*
* // async/await:
* try {
* let keys = await client.getKeysAsync();
* } catch (err) {
* console.error('Error getting keys', err);
* }
*
* @return {Promise}
*/
getKeysAsync = promisifyIt(this.getKeys, this);

/**
* Get all signing keys. Use this if you prefer to use Promises or async/await.
*
* @example
* client.getSigningKeysAsync()
* .then(keys => { console.log(`Returned {keys.length} signing keys`); })
* .catch(err => { console.error('Error getting keys', err); });
*
* // async/await:
* try {
* let keys = await client.getSigningKeysAsync();
* } catch (err) {
* console.error('Error getting signing keys', err);
* }
*
* @return {Promise}
*/
getSigningKeysAsync = promisifyIt(this.getSigningKeys, this);

/**
* Get a signing key for a specified key ID (kid). Use this if you prefer to use Promises or async/await.
*
* @example
* client.getSigningKeyId('someKid')
* .then(key => { console.log(`Signing key returned is {key.getPublicKey()}`); })
* .catch(err => { console.error('Error getting signing key', err); });
*
* // async/await:
* try {
* let key = await client.getSigningKeyAsync('someKid');
* } catch (err) {
* console.error('Error getting signing key', err);
* }
*
* @param {String} kid The Key ID of the signing key to retrieve.
*
* @return {Promise}
*/
getSigningKeyAsync = promisifyIt(this.getSigningKey, this);
}
const kidDefined = kid !== undefined && kid !== null;
if (!kidDefined && keys.length > 1) {
this.logger('No KID specified and JWKS endpoint returned more than 1 key');
throw new SigningKeyNotFoundError('No KID specified and JWKS endpoint returned more than 1 key');
}

const promisifyIt = (fn, ctx) => (...args) => {
return new Promise((resolve, reject) => {
fn.call(ctx, ...args, (err, data) => {
if (err) {
reject(err);
}
resolve(data);
});
});
};
const key = keys.find(k => !kidDefined || k.kid === kid);
if (key) {
return key;
} else {
this.logger(`Unable to find a signing key that matches '${kid}'`);
throw new SigningKeyNotFoundError(`Unable to find a signing key that matches '${kid}'`);
}
}
}
14 changes: 6 additions & 8 deletions src/integrations/express.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,11 @@ module.exports.expressJwtSecret = (options) => {
return cb(null, null);
}

client.getSigningKey(header.kid, (err, key) => {
if (err) {
return onError(err, (newError) => cb(newError, null));
}

// Provide the key.
return cb(null, key.publicKey || key.rsaPublicKey);
});
client.getSigningKey(header.kid)
.then(key => {
cb(null, key.publicKey || key.rsaPublicKey);
}).catch(err => {
onError(err, (newError) => cb(newError, null));
});
};
};
12 changes: 5 additions & 7 deletions src/integrations/hapi.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,13 +49,11 @@ module.exports.hapiJwt2Key = (options) => {
return cb(new Error('Unsupported algorithm ' + decoded.header.alg + ' supplied.'), null, null);
}

client.getSigningKey(decoded.header.kid, (err, key) => {
if (err) {
client.getSigningKey(decoded.header.kid)
.then(key => {
return cb(null, key.publicKey || key.rsaPublicKey, key);
}).catch(err => {
return onError(err, (newError) => cb(newError, null, null));
}

// Provide the key.
return cb(null, key.publicKey || key.rsaPublicKey, key);
});
});
};
};
18 changes: 6 additions & 12 deletions src/integrations/koa.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,27 +11,21 @@ module.exports.koaJwtSecret = (options = {}) => {
const client = new JwksClient(options);

return function secretProvider({ alg, kid } = {}) {

return new Promise((resolve, reject) => {

if (!supportedAlg.includes(alg)) {
return reject(new Error('Missing / invalid token algorithm'));
}

client.getSigningKey(kid, (err, key) => {
if (err) {

client.getSigningKey(kid)
.then(key => {
resolve(key.publicKey || key.rsaPublicKey);
}).catch(err => {
if (options.handleSigningKeyError) {
return options.handleSigningKeyError(err)
.then(reject);
return options.handleSigningKeyError(err).then(reject);
}

return reject(err);
}

// Provide the key.
resolve(key.publicKey || key.rsaPublicKey);
});
});
});
};
};
14 changes: 6 additions & 8 deletions src/integrations/passport.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,11 @@ module.exports.passportJwtSecret = (options) => {
return cb(null, null);
}

client.getSigningKey(decoded.header.kid, (err, key) => {
if (err) {
return onError(err, (newError) => cb(newError, null));
}

// Provide the key.
return cb(null, key.publicKey || key.rsaPublicKey);
});
client.getSigningKey(decoded.header.kid)
.then(key => {
cb(null, key.publicKey || key.rsaPublicKey);
}).catch(err => {
onError(err, (newError) => cb(newError, null));
});
};
};
15 changes: 2 additions & 13 deletions src/wrappers/cache.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,10 @@ import memoizer from 'lru-memoizer';

export default function(client, { cacheMaxEntries = 5, cacheMaxAge = 600000 } = options) {
const logger = debug('jwks');
const getSigningKey = client.getSigningKey;

logger(`Configured caching of signing keys. Max: ${cacheMaxEntries} / Age: ${cacheMaxAge}`);
return memoizer({
load: (kid, callback) => {
getSigningKey(kid, (err, key) => {
if (err) {
return callback(err);
}

logger(`Caching signing key for '${kid}':`, key);
return callback(null, key);
});
},
return memoizer.sync({
hash: (kid) => kid,
load: client.getSigningKey.bind(client),
maxAge: cacheMaxAge,
max: cacheMaxEntries
});
Expand Down
Loading