diff --git a/docs/README.md b/docs/README.md index 53ffa9fdf..b18629fbc 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1998,7 +1998,8 @@ _**default value**_: ```js { AccessToken: undefined, - ClientCredentials: undefined + ClientCredentials: undefined, + jwtAccessTokenSigningAlg: [AsyncFunction: jwtAccessTokenSigningAlg] } ```
@@ -2045,6 +2046,21 @@ Configure `formats`: ```
+### 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. diff --git a/lib/helpers/defaults.js b/lib/helpers/defaults.js index 854e2a094..e47ef1c8f 100644 --- a/lib/helpers/defaults.js +++ b/lib/helpers/defaults.js @@ -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, }, diff --git a/lib/models/formats/jwt.js b/lib/models/formats/jwt.js index e516054a8..8e62e87a1 100644 --- a/lib/models/formats/jwt.js +++ b/lib/models/formats/jwt.js @@ -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'); @@ -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 }; } @@ -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, diff --git a/test/storage/jwt.test.js b/test/storage/jwt.test.js index 49a605b09..5f02210b1 100644 --- a/test/storage/jwt.test.js +++ b/test/storage/jwt.test.js @@ -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'); + } + }); + }); }); }