Skip to content

Commit

Permalink
feat: graduate issAuthResp feature as stable and enable by default
Browse files Browse the repository at this point in the history
  • Loading branch information
panva committed Apr 20, 2022
1 parent 0b4caf1 commit e774f60
Show file tree
Hide file tree
Showing 16 changed files with 81 additions and 64 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ _Note that not all features are enabled by default, check the configuration sect
- [RFC8707 - OAuth 2.0 Resource Indicators][resource-indicators]
- [RFC9101 - OAuth 2.0 JWT-Secured Authorization Request (JAR)][jar]
- [RFC9126 - OAuth 2.0 Pushed Authorization Requests (PAR) - draft 08][par]
- [RFC9207 - OAuth 2.0 Authorization Server Issuer Identifier in Authorization Response][iss-auth-resp]
- [Financial-grade API Security Profile 1.0 - Part 2: Advanced (FAPI)][fapi]
- [OpenID Connect Client Initiated Backchannel Authentication Flow - Core 1.0 (CIBA)][ciba]

Expand All @@ -46,7 +47,6 @@ The following draft specifications are implemented by oidc-provider:
- [JWT Response for OAuth Token Introspection - draft 10][jwt-introspection]
- [JWT Secured Authorization Response Mode for OAuth 2.0 (JARM) - Implementer's Draft 01][jarm]
- [Financial-grade API: Client Initiated Backchannel Authentication Profile (FAPI-CIBA) - Implementer's Draft 01][fapi-ciba]
- [OAuth 2.0 Authorization Server Issuer Identifier in Authorization Response - draft 04][iss-auth-resp]
- [OAuth 2.0 Demonstration of Proof-of-Possession at the Application Layer (DPoP) - draft 03][dpop]
- [OpenID Connect Back-Channel Logout 1.0 - draft 06][backchannel-logout]
- [OpenID Connect RP-Initiated Logout 1.0 - draft 01][rpinitiated-logout]
Expand Down Expand Up @@ -147,7 +147,7 @@ actions and i.e. emit metrics that react to specific triggers. See the list of a
[support-sponsor]: https://github.com/sponsors/panva
[par]: https://www.rfc-editor.org/rfc/rfc9126.html
[rpinitiated-logout]: https://openid.net/specs/openid-connect-rpinitiated-1_0-01.html
[iss-auth-resp]: https://tools.ietf.org/html/draft-ietf-oauth-iss-auth-resp-04
[iss-auth-resp]: https://www.rfc-editor.org/rfc/rfc9207.html
[fapi]: https://openid.net/specs/openid-financial-api-part-2-1_0.html
[ciba]: https://openid.net/specs/openid-client-initiated-backchannel-authentication-core-1_0-final.html
[fapi-ciba]: https://openid.net/specs/openid-financial-api-ciba-ID1.html
1 change: 0 additions & 1 deletion certification/fapi/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,6 @@ const fapi = new Provider(ISSUER, {
enabled: true,
profile: process.env.PROFILE ? process.env.PROFILE : '1.0 Final',
},
issAuthResp: { enabled: true },
mTLS: {
enabled: true,
certificateBoundAccessTokens: true,
Expand Down
1 change: 0 additions & 1 deletion certification/oidc/configuration.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,6 @@ module.exports = {
features: {
backchannelLogout: { enabled: true },
devInteractions: { enabled: false },
issAuthResp: { enabled: true },
mTLS: {
enabled: true,
certificateBoundAccessTokens: true,
Expand Down
19 changes: 0 additions & 19 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -429,7 +429,6 @@ location / {
- [encryption](#featuresencryption)
- [fapi](#featuresfapi)
- [introspection](#featuresintrospection)
- [issAuthResp](#featuresissauthresp)
- [jwtIntrospection](#featuresjwtintrospection)
- [jwtResponseModes](#featuresjwtresponsemodes)
- [jwtUserinfo](#featuresjwtuserinfo)
Expand Down Expand Up @@ -1145,24 +1144,6 @@ async function introspectionAllowedPolicy(ctx, client, token) {

</details>

### features.issAuthResp

[draft-ietf-oauth-iss-auth-resp-04](https://tools.ietf.org/html/draft-ietf-oauth-iss-auth-resp-04) - OAuth 2.0 Authorization Server Issuer Identifier in Authorization Response

Enables `iss` authorization response parameter for responses without existing countermeasures against mix-up attacks.


_**recommendation**_: Updates to draft specification versions are released as MINOR library versions, if you utilize these specification implementations consider using the tilde `~` operator in your package.json since breaking changes may be introduced as part of these version updates. Alternatively, [acknowledge](#features) the version and be notified of breaking changes as part of your CI.


_**default value**_:
```js
{
ack: undefined,
enabled: false
}
```

### features.jwtIntrospection

[draft-ietf-oauth-jwt-introspection-response-10](https://tools.ietf.org/html/draft-ietf-oauth-jwt-introspection-response-10) - JWT Response for OAuth Token Introspection
Expand Down
1 change: 0 additions & 1 deletion example/support/configuration.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ module.exports = {

deviceFlow: { enabled: true }, // defaults to false
revocation: { enabled: true }, // defaults to false
issAuthResp: { enabled: true }, // defaults to false
},
jwks: {
keys: [
Expand Down
5 changes: 3 additions & 2 deletions lib/actions/authorization/respond.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,13 @@ module.exports = async function respond(ctx, next) {
out.state = params.state;
}

if (!out.id_token && instance(ctx.oidc.provider).configuration('features.issAuthResp.enabled')) {
const { responseMode } = ctx.oidc;
if (!out.id_token && !responseMode.includes('jwt')) {
out.iss = ctx.oidc.provider.issuer;
}

ctx.oidc.provider.emit('authorization.success', ctx, out);

const handler = instance(ctx.oidc.provider).responseModes.get(ctx.oidc.responseMode);
const handler = instance(ctx.oidc.provider).responseModes.get(responseMode);
await handler(ctx, params.redirect_uri, out);
};
2 changes: 1 addition & 1 deletion lib/actions/discovery.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ module.exports = function discovery(ctx, next) {
issuer: ctx.oidc.issuer,
jwks_uri: ctx.oidc.urlFor('jwks'),
registration_endpoint: features.registration.enabled ? ctx.oidc.urlFor('registration') : undefined,
authorization_response_iss_parameter_supported: features.issAuthResp.enabled ? true : undefined,
authorization_response_iss_parameter_supported: true,
response_modes_supported: ['form_post', 'fragment', 'query'],
response_types_supported: config.responseTypes,
scopes_supported: [...config.scopes],
Expand Down
16 changes: 0 additions & 16 deletions lib/helpers/defaults.js
Original file line number Diff line number Diff line change
Expand Up @@ -1834,22 +1834,6 @@ function getDefaults() {
* @skip
*/
webMessageResponseMode: { enabled: false, ack: undefined },

/*
* features.issAuthResp
*
* title: [draft-ietf-oauth-iss-auth-resp-04](https://tools.ietf.org/html/draft-ietf-oauth-iss-auth-resp-04) - OAuth 2.0 Authorization Server Issuer Identifier in Authorization Response
*
* description: Enables `iss` authorization response parameter for responses without
* existing countermeasures against mix-up attacks.
*
* recommendation: Updates to draft specification versions are released as MINOR library versions,
* if you utilize these specification implementations consider using the tilde `~` operator
* in your package.json since breaking changes may be introduced as part of these version
* updates. Alternatively, [acknowledge](#features) the version and be notified of breaking
* changes as part of your CI.
*/
issAuthResp: { enabled: false, ack: undefined },
},

/*
Expand Down
7 changes: 1 addition & 6 deletions lib/helpers/features.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const STABLE = new Set([
'encryption',
'fapi',
'introspection',
'issAuthResp',
'jwtUserinfo',
'mTLS',
'pushedAuthorizationRequests',
Expand Down Expand Up @@ -50,12 +51,6 @@ const DRAFTS = new Map(Object.entries({
url: 'https://tools.ietf.org/html/draft-sakimura-oauth-wmrm-00',
version: [0, 'id-00', 'individual-draft-00'],
},
issAuthResp: {
name: 'OAuth 2.0 Authorization Server Issuer Identifier in Authorization Response - draft 04',
type: 'IETF OAuth Working Group draft',
url: 'https://tools.ietf.org/html/draft-ietf-oauth-iss-auth-resp-04',
version: ['draft-00', 'draft-01', 'draft-02', 'draft-03', 'draft-04'],
},
}));

module.exports = {
Expand Down
5 changes: 1 addition & 4 deletions lib/shared/authorization_error_handler.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ module.exports = (provider) => {
});

function getOutAndEmit(ctx, err, state) {
const out = errOut(err, state);
const out = { ...errOut(err, state), iss: ctx.oidc.provider.issuer };

if (err.expose) {
provider.emit('authorization.error', ctx, err);
Expand Down Expand Up @@ -98,9 +98,6 @@ module.exports = (provider) => {
const renderError = instance(provider).configuration('renderError');
await renderError(ctx, out, err);
} else {
if (instance(provider).configuration('features.issAuthResp.enabled')) {
out.iss = provider.issuer;
}
let mode = safe(params.response_mode);
if (!instance(provider).responseModes.has(mode)) {
mode = resolveResponseMode(safe(params.response_type));
Expand Down
4 changes: 2 additions & 2 deletions test/custom_response_modes/custom_response_modes.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ describe('custom response modes', () => {
.expect(() => {
expect(spy.calledOnce).to.be.true;
expect(spy.firstCall.args[1]).to.equal('https://client.example.com/cb');
expect(spy.firstCall.args[2]).to.have.keys('code', 'state');
expect(spy.firstCall.args[2]).to.have.keys('code', 'state', 'iss');
});
});

Expand All @@ -49,7 +49,7 @@ describe('custom response modes', () => {
.expect(() => {
expect(spy.calledOnce).to.be.true;
expect(spy.firstCall.args[1]).to.equal('https://client.example.com/cb');
expect(spy.firstCall.args[2]).to.have.keys('error', 'error_description', 'state');
expect(spy.firstCall.args[2]).to.have.keys('error', 'error_description', 'state', 'iss');
});
});

Expand Down
1 change: 0 additions & 1 deletion test/iss/iss.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ const merge = require('lodash/merge');
const config = cloneDeep(require('../default.config'));

merge(config.features, {
issAuthResp: { enabled: true },
jwtResponseModes: { enabled: true },
});

Expand Down
2 changes: 1 addition & 1 deletion test/iss/iss.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ const { expect } = require('chai');

const bootstrap = require('../test_helper');

describe('features.issAuthResp', () => {
describe('OAuth 2.0 Authorization Server Issuer Identification', () => {
before(bootstrap(__dirname));

describe('enriched discovery', () => {
Expand Down
3 changes: 3 additions & 0 deletions test/test_helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,9 @@ module.exports = function testHelper(dir, {
absolute = all;
}

// eslint-disable-next-line no-param-reassign
keys = (!absolute || keys.includes('id_token') || keys.includes('response')) ? keys : [...new Set(keys.concat('iss'))];

return (response) => {
const { query } = parse(response.headers.location, true);
if (absolute) {
Expand Down
4 changes: 2 additions & 2 deletions test/web_message/web_message.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ module.exports = {
config,
client: {
client_id: 'client',
grant_types: ['implicit'],
response_types: ['id_token token'],
grant_types: ['implicit', 'authorization_code'],
response_types: ['code id_token token', 'code'],
redirect_uris: ['https://client.example.com'],
web_message_uris: ['https://auth.example.com'],
token_endpoint_auth_method: 'none',
Expand Down
70 changes: 65 additions & 5 deletions test/web_message/web_message.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ const bootstrap = require('../test_helper');
const { WebMessageUriMismatch } = require('../../lib/helpers/errors');

const route = '/auth';
const response_type = 'id_token token';
const response_type = 'code id_token token';
const response_mode = 'web_message';
const scope = 'openid';

Expand Down Expand Up @@ -34,7 +34,7 @@ describe('configuration features.webMessageResponseMode', () => {
before(function () { return this.login(); });
after(function () { return this.logout(); });

it('responds by rendering a an HTML with the client side code and response data [1/2]', async function () {
it('responds by rendering a an HTML with the client side code and response data [1/4]', async function () {
const auth = new this.AuthorizationRequest({
response_type,
response_mode,
Expand All @@ -57,15 +57,15 @@ describe('configuration features.webMessageResponseMode', () => {
expect(response).to.have.property('redirect_uri', auth.redirect_uri);
expect(response).to.have.property('web_message_uri', null);
expect(response).to.have.property('web_message_target', null);
expect(response.response).to.have.keys('id_token', 'state', 'access_token', 'scope', 'expires_in', 'token_type');
expect(response.response).to.have.keys('id_token', 'state', 'access_token', 'scope', 'expires_in', 'token_type', 'code');
expect(response.response.id_token).to.be.a('string');
expect(response.response.expires_in).to.be.a('number');
expect(response.response.access_token).to.be.a('string');
expect(response.response.token_type).to.equal('Bearer');
expect(response.response.state).to.equal(auth.state);
});

it('responds by rendering a an HTML with the client side code and response data [2/2]', async function () {
it('responds by rendering a an HTML with the client side code and response data [2/4]', async function () {
const auth = new this.AuthorizationRequest({
response_type,
response_mode,
Expand All @@ -90,13 +90,69 @@ describe('configuration features.webMessageResponseMode', () => {
expect(response).to.have.property('redirect_uri', auth.redirect_uri);
expect(response).to.have.property('web_message_uri', 'https://auth.example.com');
expect(response).to.have.property('web_message_target', 'targetID');
expect(response.response).to.have.keys('id_token', 'state', 'access_token', 'scope', 'expires_in', 'token_type');
expect(response.response).to.have.keys('id_token', 'state', 'access_token', 'scope', 'expires_in', 'token_type', 'code');
expect(response.response.id_token).to.be.a('string');
expect(response.response.expires_in).to.be.a('number');
expect(response.response.access_token).to.be.a('string');
expect(response.response.token_type).to.equal('Bearer');
expect(response.response.state).to.equal(auth.state);
});

it('responds by rendering a an HTML with the client side code and response data [3/4]', async function () {
const auth = new this.AuthorizationRequest({
response_type: 'code',
response_mode,
scope,
});

await this.wrap({ route, auth, verb: 'get' })
.expect(200)
.expect('pragma', 'no-cache')
.expect('cache-control', 'no-cache, no-store')
.expect('content-type', 'text/html; charset=utf-8')
.expect((response) => {
expect(response.headers['x-frame-options']).not.to.be.ok;
expect(response.headers['content-security-policy']).not.to.match(/frame-ancestors/);
})
.expect(/var data = ({[a-zA-Z0-9"{}~ ,-_]+});/);

const response = JSON.parse(RegExp.$1);
expect(response).to.have.keys('redirect_uri', 'web_message_uri', 'web_message_target', 'response');
expect(response).to.have.property('redirect_uri', auth.redirect_uri);
expect(response).to.have.property('web_message_uri', null);
expect(response).to.have.property('web_message_target', null);
expect(response.response).to.have.keys('state', 'code', 'iss');
expect(response.response.state).to.equal(auth.state);
});

it('responds by rendering a an HTML with the client side code and response data [4/4]', async function () {
const auth = new this.AuthorizationRequest({
response_type: 'code',
response_mode,
scope,
web_message_uri: 'https://auth.example.com',
web_message_target: 'targetID',
});

await this.wrap({ route, auth, verb: 'get' })
.expect(200)
.expect('pragma', 'no-cache')
.expect('cache-control', 'no-cache, no-store')
.expect('content-type', 'text/html; charset=utf-8')
.expect((response) => {
expect(response.headers['x-frame-options']).not.to.be.ok;
expect(response.headers['content-security-policy']).not.to.match(/frame-ancestors/);
})
.expect(/var data = ({[a-zA-Z0-9"{}~ ,-_]+});/);

const response = JSON.parse(RegExp.$1);
expect(response).to.have.keys('redirect_uri', 'web_message_uri', 'web_message_target', 'response');
expect(response).to.have.property('redirect_uri', auth.redirect_uri);
expect(response).to.have.property('web_message_uri', 'https://auth.example.com');
expect(response).to.have.property('web_message_target', 'targetID');
expect(response.response).to.have.keys('state', 'code', 'iss');
expect(response.response.state).to.equal(auth.state);
});
});

context('error handling', () => {
Expand All @@ -121,6 +177,7 @@ describe('configuration features.webMessageResponseMode', () => {
expect(emitSpy.calledOnce).to.be.true;
expect(renderSpy.calledOnce).to.be.true;
const renderArgs = renderSpy.args[0];
expect(renderArgs[1]).to.have.property('iss');
expect(renderArgs[1]).to.have.property('error', 'web_message_uri_mismatch');
expect(renderArgs[1]).to.have.property('error_description', "web_message_uri did not match any client's registered web_message_uris");
expect(renderArgs[2]).to.be.an.instanceof(WebMessageUriMismatch);
Expand Down Expand Up @@ -154,6 +211,7 @@ describe('configuration features.webMessageResponseMode', () => {
.expect(() => {
expect(renderSpy.calledOnce).to.be.true;
const renderArgs = renderSpy.args[0];
expect(renderArgs[1]).to.have.property('iss');
expect(renderArgs[1]).to.have.property('error', 'web_message_uri_mismatch');
expect(renderArgs[2]).to.be.an.instanceof(WebMessageUriMismatch);
});
Expand Down Expand Up @@ -192,6 +250,7 @@ describe('configuration features.webMessageResponseMode', () => {
.expect(() => {
expect(renderSpy.calledOnce).to.be.true;
const renderArgs = renderSpy.args[0];
expect(renderArgs[1]).to.have.property('iss');
expect(renderArgs[1]).to.have.property('error', 'web_message_uri_mismatch');
expect(renderArgs[2]).to.be.an.instanceof(WebMessageUriMismatch);
});
Expand Down Expand Up @@ -223,6 +282,7 @@ describe('configuration features.webMessageResponseMode', () => {
.expect(/var data = ({[a-zA-Z0-9"{} ,-_]+});/);

const { response } = JSON.parse(RegExp.$1);
expect(response).to.have.property('iss');
expect(response).to.have.property('error', 'login_required');
expect(response).to.have.property('state', auth.state);
});
Expand Down

0 comments on commit e774f60

Please sign in to comment.