Skip to content

Commit

Permalink
feat: new option for resolving JWT Access Token signing algorithm
Browse files Browse the repository at this point in the history
This now allows for an OOB setting depending on the RS capabilities
to dictate the JWT AT algorithm.
  • Loading branch information
panva committed Aug 4, 2019
1 parent e933704 commit 28e85ef
Show file tree
Hide file tree
Showing 4 changed files with 77 additions and 9 deletions.
18 changes: 17 additions & 1 deletion docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1998,7 +1998,8 @@ _**default value**_:
```js
{
AccessToken: undefined,
ClientCredentials: undefined
ClientCredentials: undefined,
jwtAccessTokenSigningAlg: [AsyncFunction: jwtAccessTokenSigningAlg]
}
```
<a name="formats-to-enable-jwt-access-tokens"></a><details>
Expand Down Expand Up @@ -2045,6 +2046,21 @@ Configure `formats`:
```
</details>

### formats.jwtAccessTokenSigningAlg

helper used by the provider to resolve a JWT Access Token signing algorithm. The resolved algorithm must be an asymmetric one supported by the provider's keys in jwks.


_**default value**_:
```js
async jwtAccessTokenSigningAlg(ctx, token, client) {
if (client && client.idTokenSignedResponseAlg !== 'none' && !client.idTokenSignedResponseAlg.startsWith('HS')) {
return client.idTokenSignedResponseAlg;
}
return 'RS256';
}
```

### httpOptions

Helper called whenever the provider calls an external HTTP(S) resource. Use to change the [got](https://github.com/sindresorhus/got/tree/v9.6.0) library's request options as they happen. This can be used to e.g. Change the request timeout option or to configure the global agent to use HTTP_PROXY and HTTPS_PROXY environment variables.
Expand Down
13 changes: 13 additions & 0 deletions lib/helpers/defaults.js
Original file line number Diff line number Diff line change
Expand Up @@ -1227,6 +1227,19 @@ const DEFAULTS = {
* ```
*/
formats: {
/*
* formats.jwtAccessTokenSigningAlg
*
* description: helper used by the provider to resolve a JWT Access Token signing algorithm.
* The resolved algorithm must be an asymmetric one supported by the provider's keys in jwks.
*/
async jwtAccessTokenSigningAlg(ctx, token, client) { // eslint-disable-line no-unused-vars
if (client && client.idTokenSignedResponseAlg !== 'none' && !client.idTokenSignedResponseAlg.startsWith('HS')) {
return client.idTokenSignedResponseAlg;
}

return 'RS256';
},
AccessToken: undefined,
ClientCredentials: undefined,
},
Expand Down
25 changes: 17 additions & 8 deletions lib/models/formats/jwt.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
const assert = require('assert');


const JWT = require('../../helpers/jwt');
const instance = require('../../helpers/weak_cache');
const nanoid = require('../../helpers/nanoid');
const base64url = require('../../helpers/base64url');
const ctxRef = require('../ctx_ref');

const opaqueFormat = require('./opaque');

Expand All @@ -15,20 +15,28 @@ function getClaim(token, claim) {
module.exports = (provider) => {
const opaque = opaqueFormat(provider);

async function getSigningAlgAndKey(clientId) {
let alg = 'RS256'; // TODO: what if RS is disabled, PS? EdDSA?
async function getSigningAlgAndKey(ctx, token, clientId) {
let client;
if (clientId) {
client = await provider.Client.find(clientId);
assert(client);
if (client.idTokenSignedResponseAlg !== 'none' && !client.idTokenSignedResponseAlg.startsWith('HS')) {
alg = client.idTokenSignedResponseAlg;
}
}

const { keystore } = instance(provider);
const { keystore, configuration } = instance(provider);
const { formats: { jwtAccessTokenSigningAlg } } = configuration();

const alg = await jwtAccessTokenSigningAlg(ctx, token, client);

if (alg === 'none' || alg.startsWith('HS')) {
throw new Error('JWT Access Tokens may not use JWA HMAC algorithms or "none"');
}

const key = keystore.get({ alg, use: 'sig' });

if (!key) {
throw new Error('invalid alg resolved for JWT Access Token signature, the alg must be an asymmetric one that the provider has in its keystore');
}

return { key, alg };
}

Expand All @@ -46,7 +54,8 @@ module.exports = (provider) => {
if (this.jwt) {
value = this.jwt;
} else {
const { key, alg } = await getSigningAlgAndKey(azp);
const ctx = ctxRef.get(this);
const { key, alg } = await getSigningAlgAndKey(ctx, this, azp);
const tokenPayload = {
...extra,
jti,
Expand Down
30 changes: 30 additions & 0 deletions test/storage/jwt.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -350,5 +350,35 @@ if (FORMAT === 'jwt') {
jti,
});
});

describe('invalid signing alg resolved', () => {
before(bootstrap(__dirname));

['none', 'HS256', 'HS384', 'HS512'].forEach((alg) => {
it(`throws an Error when ${alg} is resolved`, async function () {
i(this.provider).configuration('formats').jwtAccessTokenSigningAlg = async () => alg;
const token = new this.provider.AccessToken(fullPayload);
try {
await token.save();
throw new Error('expected to fail');
} catch (err) {
expect(err).to.be.an('error');
expect(err.message).to.equal('JWT Access Tokens may not use JWA HMAC algorithms or "none"');
}
});
});

it('throws an Error when unsupported provider keystore alg is resolved', async function () {
i(this.provider).configuration('formats').jwtAccessTokenSigningAlg = async () => 'ES384';
const token = new this.provider.AccessToken(fullPayload);
try {
await token.save();
throw new Error('expected to fail');
} catch (err) {
expect(err).to.be.an('error');
expect(err.message).to.equal('invalid alg resolved for JWT Access Token signature, the alg must be an asymmetric one that the provider has in its keystore');
}
});
});
});
}

0 comments on commit 28e85ef

Please sign in to comment.