diff --git a/lib/actions/userinfo.js b/lib/actions/userinfo.js index 37eb94806..d459196fa 100644 --- a/lib/actions/userinfo.js +++ b/lib/actions/userinfo.js @@ -1,4 +1,4 @@ -const { InvalidDpopProof, InvalidToken, InsufficientScope } = require('../helpers/errors'); +const { InvalidToken, InsufficientScope, InvalidDpopProof } = require('../helpers/errors'); const difference = require('../helpers/_/difference'); const setWWWAuthenticate = require('../helpers/set_www_authenticate'); const bodyParser = require('../shared/conditional_body'); @@ -43,7 +43,8 @@ module.exports = [ } if (err instanceof InvalidDpopProof) { - err.error = err.message = 'invalid_token'; // eslint-disable-line no-multi-assign + // eslint-disable-next-line no-multi-assign + err.status = err.statusCode = 401; } setWWWAuthenticate(ctx, scheme, { diff --git a/lib/helpers/validate_dpop.js b/lib/helpers/validate_dpop.js index 99c883106..959a17d5a 100644 --- a/lib/helpers/validate_dpop.js +++ b/lib/helpers/validate_dpop.js @@ -44,21 +44,21 @@ module.exports = async (ctx, accessToken) => { ); if (typeof payload.jti !== 'string' || !payload.jti) { - throw new Error('must have a jti string property'); + throw new InvalidDpopProof('DPoP Proof must have a jti string property'); } if (payload.htm !== ctx.method) { - throw new Error('htm mismatch'); + throw new InvalidDpopProof('DPoP Proof htm mismatch'); } if (payload.htu !== ctx.oidc.urlFor(ctx.oidc.route)) { - throw new Error('htu mismatch'); + throw new InvalidDpopProof('DPoP Proof htu mismatch'); } if (accessToken) { const ath = base64url.encode(createHash('sha256').update(accessToken).digest()); if (payload.ath !== ath) { - throw new Error('ath mismatch'); + throw new InvalidDpopProof('DPoP Proof ath mismatch'); } } @@ -66,6 +66,9 @@ module.exports = async (ctx, accessToken) => { return { thumbprint, jti: payload.jti, iat: payload.iat }; } catch (err) { + if (err instanceof InvalidDpopProof) { + throw err; + } throw new InvalidDpopProof('invalid DPoP key binding', err.message); } }; diff --git a/test/dpop/dpop.test.js b/test/dpop/dpop.test.js index 04ddd1e3d..d39892a1b 100644 --- a/test/dpop/dpop.test.js +++ b/test/dpop/dpop.test.js @@ -118,10 +118,10 @@ describe('features.dPoP', () => { await this.agent.get('/me') // eslint-disable-line no-await-in-loop .set('DPoP', JWT.sign({}, key, { kid: false, header: { jwk: key, typ: value } })) .set('Authorization', `DPoP ${dpop}`) - .expect(400) - .expect({ error: 'invalid_token', error_description: 'invalid DPoP key binding' }) + .expect(401) + .expect({ error: 'invalid_dpop_proof', error_description: 'invalid DPoP key binding' }) .expect('WWW-Authenticate', /^DPoP /) - .expect('WWW-Authenticate', /error="invalid_token"/) + .expect('WWW-Authenticate', /error="invalid_dpop_proof"/) .expect('WWW-Authenticate', /algs="ES256 PS256"/); } @@ -129,10 +129,10 @@ describe('features.dPoP', () => { await this.agent.get('/me') // eslint-disable-line no-await-in-loop .set('DPoP', `${base64url.encode(JSON.stringify({ jwk: key, typ: 'dpop+jwt', alg: value }))}.e30.`) .set('Authorization', `DPoP ${dpop}`) - .expect(400) - .expect({ error: 'invalid_token', error_description: 'invalid DPoP key binding' }) + .expect(401) + .expect({ error: 'invalid_dpop_proof', error_description: 'invalid DPoP key binding' }) .expect('WWW-Authenticate', /^DPoP /) - .expect('WWW-Authenticate', /error="invalid_token"/) + .expect('WWW-Authenticate', /error="invalid_dpop_proof"/) .expect('WWW-Authenticate', /algs="ES256 PS256"/); } @@ -140,56 +140,56 @@ describe('features.dPoP', () => { await this.agent.get('/me') // eslint-disable-line no-await-in-loop .set('DPoP', JWT.sign({}, key, { kid: false, header: { typ: 'dpop+jwt', jwk: value } })) .set('Authorization', `DPoP ${dpop}`) - .expect(400) - .expect({ error: 'invalid_token', error_description: 'invalid DPoP key binding' }) + .expect(401) + .expect({ error: 'invalid_dpop_proof', error_description: 'invalid DPoP key binding' }) .expect('WWW-Authenticate', /^DPoP /) - .expect('WWW-Authenticate', /error="invalid_token"/) + .expect('WWW-Authenticate', /error="invalid_dpop_proof"/) .expect('WWW-Authenticate', /algs="ES256 PS256"/); } await this.agent.get('/me') // eslint-disable-line no-await-in-loop .set('DPoP', JWT.sign({}, key, { kid: false, header: { typ: 'dpop+jwt', jwk: key.toJWK(true) } })) .set('Authorization', `DPoP ${dpop}`) - .expect(400) - .expect({ error: 'invalid_token', error_description: 'invalid DPoP key binding' }) + .expect(401) + .expect({ error: 'invalid_dpop_proof', error_description: 'invalid DPoP key binding' }) .expect('WWW-Authenticate', /^DPoP /) - .expect('WWW-Authenticate', /error="invalid_token"/) + .expect('WWW-Authenticate', /error="invalid_dpop_proof"/) .expect('WWW-Authenticate', /algs="ES256 PS256"/); await this.agent.get('/me') // eslint-disable-line no-await-in-loop .set('DPoP', JWT.sign({}, key, { kid: false, header: { typ: 'dpop+jwt', jwk: await JWK.generate('oct') } })) .set('Authorization', `DPoP ${dpop}`) - .expect(400) - .expect({ error: 'invalid_token', error_description: 'invalid DPoP key binding' }) + .expect(401) + .expect({ error: 'invalid_dpop_proof', error_description: 'invalid DPoP key binding' }) .expect('WWW-Authenticate', /^DPoP /) - .expect('WWW-Authenticate', /error="invalid_token"/) + .expect('WWW-Authenticate', /error="invalid_dpop_proof"/) .expect('WWW-Authenticate', /algs="ES256 PS256"/); await this.agent.get('/me') // eslint-disable-line no-await-in-loop .set('DPoP', JWT.sign({ htm: 'POST', htu: `${this.provider.issuer}${this.suitePath('/me')}` }, key, { kid: false, header: { typ: 'dpop+jwt', jwk: key } })) .set('Authorization', `DPoP ${dpop}`) - .expect(400) - .expect({ error: 'invalid_token', error_description: 'invalid DPoP key binding' }) + .expect(401) + .expect({ error: 'invalid_dpop_proof', error_description: 'DPoP Proof must have a jti string property' }) .expect('WWW-Authenticate', /^DPoP /) - .expect('WWW-Authenticate', /error="invalid_token"/) + .expect('WWW-Authenticate', /error="invalid_dpop_proof"/) .expect('WWW-Authenticate', /algs="ES256 PS256"/); await this.agent.get('/me') // eslint-disable-line no-await-in-loop .set('DPoP', JWT.sign({ jti: 'foo', htm: 'POST' }, key, { kid: false, header: { typ: 'dpop+jwt', jwk: key } })) .set('Authorization', `DPoP ${dpop}`) - .expect(400) - .expect({ error: 'invalid_token', error_description: 'invalid DPoP key binding' }) + .expect(401) + .expect({ error: 'invalid_dpop_proof', error_description: 'DPoP Proof htm mismatch' }) .expect('WWW-Authenticate', /^DPoP /) - .expect('WWW-Authenticate', /error="invalid_token"/) + .expect('WWW-Authenticate', /error="invalid_dpop_proof"/) .expect('WWW-Authenticate', /algs="ES256 PS256"/); await this.agent.get('/me') // eslint-disable-line no-await-in-loop .set('DPoP', JWT.sign({ jti: 'foo', htm: 'GET', htu: 'foo' }, key, { kid: false, header: { typ: 'dpop+jwt', jwk: key } })) .set('Authorization', `DPoP ${dpop}`) - .expect(400) - .expect({ error: 'invalid_token', error_description: 'invalid DPoP key binding' }) + .expect(401) + .expect({ error: 'invalid_dpop_proof', error_description: 'DPoP Proof htu mismatch' }) .expect('WWW-Authenticate', /^DPoP /) - .expect('WWW-Authenticate', /error="invalid_token"/) + .expect('WWW-Authenticate', /error="invalid_dpop_proof"/) .expect('WWW-Authenticate', /algs="ES256 PS256"/); await this.agent.get('/me') // eslint-disable-line no-await-in-loop @@ -197,10 +197,10 @@ describe('features.dPoP', () => { jti: 'foo', htm: 'GET', htu: `${this.provider.issuer}${this.suitePath('/me')}`, iat: epochTime() - 61, }, key, { kid: false, iat: false, header: { typ: 'dpop+jwt', jwk: key } })) .set('Authorization', `DPoP ${dpop}`) - .expect(400) - .expect({ error: 'invalid_token', error_description: 'invalid DPoP key binding' }) + .expect(401) + .expect({ error: 'invalid_dpop_proof', error_description: 'invalid DPoP key binding' }) .expect('WWW-Authenticate', /^DPoP /) - .expect('WWW-Authenticate', /error="invalid_token"/) + .expect('WWW-Authenticate', /error="invalid_dpop_proof"/) .expect('WWW-Authenticate', /algs="ES256 PS256"/); await this.agent.get('/me') // eslint-disable-line no-await-in-loop @@ -208,10 +208,10 @@ describe('features.dPoP', () => { jti: 'foo', htm: 'GET', htu: `${this.provider.issuer}${this.suitePath('/me')}`, }, key, { kid: false, header: { typ: 'dpop+jwt', jwk: await JWK.generate('EC') } })) .set('Authorization', `DPoP ${dpop}`) - .expect(400) - .expect({ error: 'invalid_token', error_description: 'invalid DPoP key binding' }) + .expect(401) + .expect({ error: 'invalid_dpop_proof', error_description: 'invalid DPoP key binding' }) .expect('WWW-Authenticate', /^DPoP /) - .expect('WWW-Authenticate', /error="invalid_token"/) + .expect('WWW-Authenticate', /error="invalid_dpop_proof"/) .expect('WWW-Authenticate', /algs="ES256 PS256"/); }); @@ -258,8 +258,8 @@ describe('features.dPoP', () => { await this.agent.get('/me') .set('Authorization', `DPoP ${dpop}`) .set('DPoP', this.proof(`${this.provider.issuer}${this.suitePath('/me')}`, 'GET', 'anotherAccessTokenValue')) - .expect({ error: 'invalid_token', error_description: 'invalid DPoP key binding' }) - .expect(400); + .expect({ error: 'invalid_dpop_proof', error_description: 'DPoP Proof ath mismatch' }) + .expect(401); expect(spy).to.have.property('calledOnce', true); expect(spy.args[0][1]).to.have.property('error_detail', 'failed jkt verification'); @@ -626,4 +626,16 @@ describe('features.dPoP', () => { expect(ClientCredentials).to.have.property('jkt', expectedS256); }); }); + + describe('status codes at the token endpoint', () => { + it('should be 400 for invalid_dpop_proof', async function () { + return this.agent.post('/token') + .auth('client', 'secret') + .send({ grant_type: 'client_credentials' }) + .set('DPoP', 'invalid') + .type('form') + .expect(400) + .expect({ error: 'invalid_dpop_proof', error_description: 'invalid DPoP key binding' }); + }); + }); });