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');
+ }
+ });
+ });
});
}