Skip to content

Commit

Permalink
feat: support OAuth 2.0 Authorization Server Issuer Identification
Browse files Browse the repository at this point in the history
Added support for
[draft-ietf-oauth-iss-auth-resp](https://datatracker.ietf.org/doc/html/draft-ietf-oauth-iss-auth-resp-04)
  • Loading branch information
panva committed Dec 3, 2021
1 parent f72bc4a commit fb6a141
Show file tree
Hide file tree
Showing 3 changed files with 115 additions and 14 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ openid-client.
- [Financial-grade API Security Profile 1.0 - Part 2: Advanced (FAPI)][feature-fapi]
- [JWT Secured Authorization Response Mode for OAuth 2.0 (JARM) - ID1][feature-jarm]
- [OAuth 2.0 Demonstration of Proof-of-Possession at the Application Layer (DPoP) - draft 03][feature-dpop]
- [OAuth 2.0 Authorization Server Issuer Identification - draft-04][feature-iss]

Updates to draft specifications (DPoP, JARM, etc) are released as MINOR library versions,
if you utilize these specification implementations consider using the tilde `~` operator in your
Expand Down Expand Up @@ -279,6 +280,7 @@ See [Customizing (docs)][documentation-customizing].
[feature-dpop]: https://tools.ietf.org/html/draft-ietf-oauth-dpop-03
[feature-par]: https://www.rfc-editor.org/rfc/rfc9126.html
[feature-jar]: https://www.rfc-editor.org/rfc/rfc9101.html
[feature-iss]: https://datatracker.ietf.org/doc/html/draft-ietf-oauth-iss-auth-resp-04
[openid-certified-link]: https://openid.net/certification/
[passport-url]: http://passportjs.org
[npm-url]: https://www.npmjs.com/package/openid-client
Expand Down
45 changes: 42 additions & 3 deletions lib/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,15 +41,16 @@ function pickCb(input) {
input,
'access_token', // OAuth 2.0
'code', // OAuth 2.0
'error', // OAuth 2.0
'error_description', // OAuth 2.0
'error_uri', // OAuth 2.0
'error', // OAuth 2.0
'expires_in', // OAuth 2.0
'id_token', // OIDC Core 1.0
'iss', // draft-ietf-oauth-iss-auth-resp
'response', // FAPI JARM
'session_state', // OIDC Session Management
'state', // OAuth 2.0
'token_type', // OAuth 2.0
'session_state', // OIDC Session Management
'response', // FAPI JARM
);
}

Expand Down Expand Up @@ -396,6 +397,25 @@ class BaseClient {
});
}

if ('iss' in params) {
assertIssuerConfiguration(this.issuer, 'issuer');
if (params.iss !== this.issuer.issuer) {
throw new RPError({
printf: ['iss mismatch, expected %s, got: %s', this.issuer.issuer, params.iss],
params,
});
}
} else if (
this.issuer.authorization_response_iss_parameter_supported &&
!('id_token' in params) &&
!('response' in parameters)
) {
throw new RPError({
message: 'iss missing from the response',
params,
});
}

if (params.error) {
throw new OPError(params);
}
Expand Down Expand Up @@ -522,6 +542,25 @@ class BaseClient {
});
}

if ('iss' in params) {
assertIssuerConfiguration(this.issuer, 'issuer');
if (params.iss !== this.issuer.issuer) {
throw new RPError({
printf: ['iss mismatch, expected %s, got: %s', this.issuer.issuer, params.iss],
params,
});
}
} else if (
this.issuer.authorization_response_iss_parameter_supported &&
!('id_token' in params) &&
!('response' in parameters)
) {
throw new RPError({
message: 'iss missing from the response',
params,
});
}

if (params.error) {
throw new OPError(params);
}
Expand Down
82 changes: 71 additions & 11 deletions test/client/client_instance.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -415,6 +415,11 @@ describe('Client', () => {
issuer: 'https://op.example.com',
token_endpoint: 'https://op.example.com/token',
});
this.issuerWithIssResponse = new Issuer({
issuer: 'https://op.example.com',
token_endpoint: 'https://op.example.com/token',
authorization_response_iss_parameter_supported: true,
});
this.client = new this.issuer.Client({
client_id: 'identifier',
client_secret: 'secure',
Expand Down Expand Up @@ -537,7 +542,7 @@ describe('Client', () => {

describe('jarm response mode', function () {
it('consumes JARM responses', async function () {
const client = new this.issuer.Client({
const client = new this.issuerWithIssResponse.Client({
client_id: 'identifier',
client_secret: 'secure',
authorization_signed_response_alg: 'HS256',
Expand All @@ -549,7 +554,7 @@ describe('Client', () => {
},
client.client_secret,
{
issuer: this.issuer.issuer,
issuer: this.issuerWithIssResponse.issuer,
audience: client.client_id,
expiresIn: '5m',
},
Expand Down Expand Up @@ -588,7 +593,7 @@ describe('Client', () => {
});

it('consumes encrypted JARM responses', async function () {
const client = new this.issuer.Client({
const client = new this.issuerWithIssResponse.Client({
client_id: 'identifier',
client_secret: 'secure',
authorization_signed_response_alg: 'HS256',
Expand All @@ -603,7 +608,7 @@ describe('Client', () => {
},
client.client_secret,
{
issuer: this.issuer.issuer,
issuer: this.issuerWithIssResponse.issuer,
audience: client.client_id,
expiresIn: '5m',
},
Expand Down Expand Up @@ -665,7 +670,7 @@ describe('Client', () => {
});

it('verifies the JARM alg', async function () {
const client = new this.issuer.Client({
const client = new this.issuerWithIssResponse.Client({
client_id: 'identifier',
client_secret: 'secure',
authorization_signed_response_alg: 'HS256',
Expand All @@ -677,7 +682,7 @@ describe('Client', () => {
},
client.client_secret,
{
issuer: this.issuer.issuer,
issuer: this.issuerWithIssResponse.issuer,
audience: client.client_id,
expiresIn: '5m',
},
Expand Down Expand Up @@ -813,6 +818,11 @@ describe('Client', () => {
issuer: 'https://op.example.com',
token_endpoint: 'https://op.example.com/token',
});
this.issuerWithIssResponse = new Issuer({
issuer: 'https://op.example.com',
token_endpoint: 'https://op.example.com/token',
authorization_response_iss_parameter_supported: true,
});
this.client = new this.issuer.Client({
client_id: 'identifier',
client_secret: 'secure',
Expand Down Expand Up @@ -856,9 +866,59 @@ describe('Client', () => {
});
});

describe('OAuth 2.0 Authorization Server Issuer Identification', function () {
it('iss mismatch in oauthCallback()', function () {
return this.client
.oauthCallback(undefined, {
iss: 'https://other-op.example.com',
})
.then(fail, (error) => {
expect(error).to.be.instanceof(Error);
expect(error).to.have.property(
'message',
'iss mismatch, expected https://op.example.com, got: https://other-op.example.com',
);
});
});

it('iss mismatch in callback()', function () {
return this.client
.callback(undefined, {
iss: 'https://other-op.example.com',
})
.then(fail, (error) => {
expect(error).to.be.instanceof(Error);
expect(error).to.have.property(
'message',
'iss mismatch, expected https://op.example.com, got: https://other-op.example.com',
);
});
});

it('iss missing in oauthCallback()', function () {
const client = new this.issuerWithIssResponse.Client({
client_id: 'identifier',
});
return client.oauthCallback(undefined, {}).then(fail, (error) => {
expect(error).to.be.instanceof(Error);
expect(error).to.have.property('message', 'iss missing from the response');
});
});

it('iss missing in callback()', function () {
const client = new this.issuerWithIssResponse.Client({
client_id: 'identifier',
});
return client.callback(undefined, {}).then(fail, (error) => {
expect(error).to.be.instanceof(Error);
expect(error).to.have.property('message', 'iss missing from the response');
});
});
});

describe('jarm response mode', function () {
it('consumes JARM responses', async function () {
const client = new this.issuer.Client({
const client = new this.issuerWithIssResponse.Client({
client_id: 'identifier',
client_secret: 'secure',
authorization_signed_response_alg: 'HS256',
Expand All @@ -870,7 +930,7 @@ describe('Client', () => {
},
client.client_secret,
{
issuer: this.issuer.issuer,
issuer: this.issuerWithIssResponse.issuer,
audience: client.client_id,
expiresIn: '5m',
},
Expand Down Expand Up @@ -909,7 +969,7 @@ describe('Client', () => {
});

it('consumes encrypted JARM responses', async function () {
const client = new this.issuer.Client({
const client = new this.issuerWithIssResponse.Client({
client_id: 'identifier',
client_secret: 'secure',
authorization_signed_response_alg: 'HS256',
Expand All @@ -924,7 +984,7 @@ describe('Client', () => {
},
client.client_secret,
{
issuer: this.issuer.issuer,
issuer: this.issuerWithIssResponse.issuer,
audience: client.client_id,
expiresIn: '5m',
},
Expand Down Expand Up @@ -986,7 +1046,7 @@ describe('Client', () => {
});

it('verifies the JARM alg', async function () {
const client = new this.issuer.Client({
const client = new this.issuerWithIssResponse.Client({
client_id: 'identifier',
client_secret: 'secure',
authorization_signed_response_alg: 'HS256',
Expand Down

0 comments on commit fb6a141

Please sign in to comment.