diff --git a/lib/client.js b/lib/client.js index 85b24380..e7a57163 100644 --- a/lib/client.js +++ b/lib/client.js @@ -988,6 +988,17 @@ module.exports = (issuer, aadIssValidation = false) => class Client extends Base if (tokenset.id_token) { await this.decryptIdToken(tokenset); await this.validateIdToken(tokenset, null, 'token', null); + + if (refreshToken instanceof TokenSet && refreshToken.id_token) { + const expectedSub = refreshToken.claims().sub; + const actualSub = tokenset.claims().sub; + if (actualSub !== expectedSub) { + throw new RPError({ + printf: ['sub mismatch, expected %s, got: %s', expectedSub, actualSub], + jwt: tokenset.id_token, + }); + } + } } return tokenset; diff --git a/test/client/client_instance.test.js b/test/client/client_instance.test.js index 719a8239..db7f0c56 100644 --- a/test/client/client_instance.test.js +++ b/test/client/client_instance.test.js @@ -873,11 +873,13 @@ describe('Client', () => { describe('#refresh', function () { before(function () { const issuer = new Issuer({ + issuer: 'https://op.example.com', token_endpoint: 'https://op.example.com/token', }); this.client = new issuer.Client({ client_id: 'identifier', client_secret: 'secure', + id_token_signed_response_alg: 'HS256', }); }); @@ -931,6 +933,65 @@ describe('Client', () => { }); }); + it('passes ID Token validations when ID Token is returned', function () { + nock('https://op.example.com') + .post('/token') // to make sure filteringRequestBody works + .reply(200, { + access_token: 'present', + refresh_token: 'refreshValue', + id_token: jose.JWT.sign({ + sub: 'foo', + }, this.client.client_secret, { + issuer: this.client.issuer.issuer, + audience: this.client.client_id, + expiresIn: '5m', + }), + }); + + return this.client.refresh(new TokenSet({ + access_token: 'present', + refresh_token: 'refreshValue', + id_token: jose.JWT.sign({ + sub: 'foo', + }, this.client.client_secret, { + issuer: this.client.issuer.issuer, + audience: this.client.client_id, + expiresIn: '6m', + }), + })); + }); + + it('rejects when returned ID Token sub does not match the one passed in', function () { + nock('https://op.example.com') + .post('/token') // to make sure filteringRequestBody works + .reply(200, { + access_token: 'present', + refresh_token: 'refreshValue', + id_token: jose.JWT.sign({ + sub: 'bar', + }, this.client.client_secret, { + issuer: this.client.issuer.issuer, + audience: this.client.client_id, + expiresIn: '5m', + }), + }); + + return this.client.refresh(new TokenSet({ + access_token: 'present', + refresh_token: 'refreshValue', + id_token: jose.JWT.sign({ + sub: 'foo', + }, this.client.client_secret, { + issuer: this.client.issuer.issuer, + audience: this.client.client_id, + expiresIn: '5m', + }), + })) + .then(fail, (error) => { + expect(error).to.have.property('message', 'sub mismatch, expected foo, got: bar'); + }); + }); + it('rejects when passed a TokenSet not containing refresh_token', function () { return this.client.refresh(new TokenSet({ access_token: 'present',