diff --git a/docs/README.md b/docs/README.md index 099af6ba..b77fc3f9 100644 --- a/docs/README.md +++ b/docs/README.md @@ -211,6 +211,8 @@ Creates a new Client with the provided metadata - `jwks`: `` JWK Set formatted object with private keys used for signing client assertions or decrypting responses. - `options`: `` additional options for the client + - `additionalAuthorizedParties`: `` | `string[]` additional accepted values for the + Authorized Party (`azp`) claim. **Default:** only the client's client_id value is accepted. - Returns: `` --- @@ -488,6 +490,8 @@ Performs Dynamic Client Registration with the provided metadata at the issuer's public parts will be registered as `jwks`. - `initialAccessToken`: `` Initial Access Token to use as a Bearer token during the registration call. + - `additionalAuthorizedParties`: `` | `string[]` additional accepted values for the + Authorized Party (`azp`) claim. **Default:** only the client's client_id value is accepted. --- @@ -501,6 +505,8 @@ Performs Dynamic Client Read Request to retrieve a Client instance. - `jwks`: `` JWK Set formatted object with private keys used for signing client assertions or decrypting responses. - `clientOptions`: `` additional options passed to the `Client` constructor + - `additionalAuthorizedParties`: `` | `string[]` additional accepted values for the + Authorized Party (`azp`) claim. **Default:** only the client's client_id value is accepted. --- diff --git a/lib/client.js b/lib/client.js index 5d0aad2a..85b24380 100644 --- a/lib/client.js +++ b/lib/client.js @@ -907,11 +907,23 @@ module.exports = (issuer, aadIssValidation = false) => class Client extends Base } } - if (payload.azp !== undefined && payload.azp !== this.client_id) { - throw new RPError({ - printf: ['azp must be the client_id, expected %s, got: %s', this.client_id, payload.azp], - jwt, - }); + if (payload.azp !== undefined) { + let { additionalAuthorizedParties } = instance(this).get('options') || {}; + + if (typeof additionalAuthorizedParties === 'string') { + additionalAuthorizedParties = [this.client_id, additionalAuthorizedParties]; + } else if (Array.isArray(additionalAuthorizedParties)) { + additionalAuthorizedParties = [this.client_id, ...additionalAuthorizedParties]; + } else { + additionalAuthorizedParties = [this.client_id]; + } + + if (!additionalAuthorizedParties.includes(payload.azp)) { + throw new RPError({ + printf: ['azp mismatch, got: %s', payload.azp], + jwt, + }); + } } let key; diff --git a/test/client/client_instance.test.js b/test/client/client_instance.test.js index 9456859a..719a8239 100644 --- a/test/client/client_instance.test.js +++ b/test/client/client_instance.test.js @@ -1845,6 +1845,14 @@ describe('Client', () => { client_id: 'identifier', client_secret: 'its gotta be a long secret and i mean at least 32 characters', }); + this.clientWith3rdParty = new this.issuer.Client({ + client_id: 'identifier', + client_secret: 'its gotta be a long secret and i mean at least 32 characters', + }, undefined, { additionalAuthorizedParties: 'authorized third party' }); + this.clientWith3rdParties = new this.issuer.Client({ + client_id: 'identifier', + client_secret: 'its gotta be a long secret and i mean at least 32 characters', + }, undefined, { additionalAuthorizedParties: ['authorized third party', 'another third party'] }); this.fapiClient = new this.issuer.FAPIClient({ client_id: 'identifier', @@ -1980,7 +1988,7 @@ describe('Client', () => { return this.IdToken(this.keystore.get(), 'RS256', payload) .then((token) => this.client.validateIdToken(token)) .then(fail, (error) => { - expect(error).to.have.property('message', 'azp must be the client_id, expected identifier, got: not the client'); + expect(error).to.have.property('message', 'azp mismatch, got: not the client'); }); }); @@ -2014,6 +2022,96 @@ describe('Client', () => { .then((token) => this.client.validateIdToken(token)); }); + it('rejects unknown additional party azp values (single additional value)', function () { + const payload = { + iss: this.issuer.issuer, + sub: 'userId', + aud: [this.client.client_id, 'someone else'], + azp: 'some unknown third party', + exp: now() + 3600, + iat: now(), + }; + + return this.IdToken(this.keystore.get(), 'RS256', payload) + .then((token) => this.clientWith3rdParty.validateIdToken(token)) + .then(fail, (error) => { + expect(error).to.have.property('message', 'azp mismatch, got: some unknown third party'); + }); + }); + + it('allows configured additional party azp value (single additional value)', function () { + const payload = { + iss: this.issuer.issuer, + sub: 'userId', + aud: [this.client.client_id, 'someone else'], + azp: 'authorized third party', + exp: now() + 3600, + iat: now(), + }; + + return this.IdToken(this.keystore.get(), 'RS256', payload) + .then((token) => this.clientWith3rdParty.validateIdToken(token)); + }); + + it('allows the default (client_id) additional party azp value (single additional value)', function () { + const payload = { + iss: this.issuer.issuer, + sub: 'userId', + aud: [this.client.client_id, 'someone else'], + azp: this.client.client_id, + exp: now() + 3600, + iat: now(), + }; + + return this.IdToken(this.keystore.get(), 'RS256', payload) + .then((token) => this.clientWith3rdParty.validateIdToken(token)); + }); + + it('rejects unknown additional party azp values (multiple additional values)', function () { + const payload = { + iss: this.issuer.issuer, + sub: 'userId', + aud: [this.client.client_id, 'someone else'], + azp: 'some unknown third party', + exp: now() + 3600, + iat: now(), + }; + + return this.IdToken(this.keystore.get(), 'RS256', payload) + .then((token) => this.clientWith3rdParties.validateIdToken(token)) + .then(fail, (error) => { + expect(error).to.have.property('message', 'azp mismatch, got: some unknown third party'); + }); + }); + + it('allows configured additional party azp value (multiple additional values)', function () { + const payload = { + iss: this.issuer.issuer, + sub: 'userId', + aud: [this.client.client_id, 'someone else'], + azp: 'authorized third party', + exp: now() + 3600, + iat: now(), + }; + + return this.IdToken(this.keystore.get(), 'RS256', payload) + .then((token) => this.clientWith3rdParties.validateIdToken(token)); + }); + + it('allows the default (client_id) additional party azp value (multiple additional values)', function () { + const payload = { + iss: this.issuer.issuer, + sub: 'userId', + aud: [this.client.client_id, 'someone else'], + azp: this.client.client_id, + exp: now() + 3600, + iat: now(), + }; + + return this.IdToken(this.keystore.get(), 'RS256', payload) + .then((token) => this.clientWith3rdParties.validateIdToken(token)); + }); + it('verifies the audience when string', function () { const payload = { iss: this.issuer.issuer, diff --git a/types/index.d.ts b/types/index.d.ts index 64435ad9..c19dd6d6 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -335,6 +335,7 @@ export interface IntrospectionResponse { } export interface ClientOptions { + additionalAuthorizedParties?: false | string | string[]; } /**