diff --git a/docs/README.md b/docs/README.md index decada9ac..48efb1f2b 100644 --- a/docs/README.md +++ b/docs/README.md @@ -491,10 +491,8 @@ location / { - [httpOptions](#httpoptions) - [interactions ❗](#interactions) - [issueRefreshToken](#issuerefreshtoken) -- [logoutSource](#logoutsource) - [pairwiseIdentifier](#pairwiseidentifier) - [pkce](#pkce) -- [postLogoutSuccessSource](#postlogoutsuccesssource) - [renderError](#rendererror) - [responseTypes](#responsetypes) - [rotateRefreshToken](#rotaterefreshtoken) @@ -1725,6 +1723,83 @@ _**default value**_: } ``` +### features.rpInitiatedLogout + +[RP-Initiated Logout 1.0](https://openid.net/specs/openid-connect-rpinitiated-1_0-01.html) + +Enables RP-Initiated Logout features + + +_**default value**_: +```js +{ + enabled: true, + logoutSource: [AsyncFunction: logoutSource], // see expanded details below + postLogoutSuccessSource: [AsyncFunction: postLogoutSuccessSource] // see expanded details below +} +``` + +
(Click to expand) features.rpInitiatedLogout options details
+ + +#### logoutSource + +HTML source rendered when session management feature renders a confirmation prompt for the User-Agent. + + +_**default value**_: +```js +async function logoutSource(ctx, form) { + // @param ctx - koa request context + // @param form - form source (id="op.logoutForm") to be embedded in the page and submitted by + // the End-User + ctx.body = ` + + Logout Request + + + +
+

Do you want to sign-out from ${ctx.host}?

+ ${form} + + +
+ + `; +} +``` + +#### postLogoutSuccessSource + +HTML source rendered when session management feature concludes a logout but there was no `post_logout_redirect_uri` provided by the client. + + +_**default value**_: +```js +async function postLogoutSuccessSource(ctx) { + // @param ctx - koa request context + const { + clientId, clientName, clientUri, initiateLoginUri, logoUri, policyUri, tosUri, + } = ctx.oidc.client || {}; // client is defined if the user chose to stay logged in with the OP + const display = clientName || clientId; + ctx.body = ` + + Sign-out Success + + + +
+

Sign-out Success

+

Your sign-out ${display ? `with ${display}` : ''} was successful.

+
+ + `; +} +``` + +
+ ### features.sessionManagement [Session Management 1.0 - draft 30](https://openid.net/specs/openid-connect-session-1_0-30.html) @@ -2793,34 +2868,6 @@ async issueRefreshToken(ctx, client, code) { ``` -### logoutSource - -HTML source rendered when session management feature renders a confirmation prompt for the User-Agent. - - -_**default value**_: -```js -async function logoutSource(ctx, form) { - // @param ctx - koa request context - // @param form - form source (id="op.logoutForm") to be embedded in the page and submitted by - // the End-User - ctx.body = ` - - Logout Request - - - -
-

Do you want to sign-out from ${ctx.host}?

- ${form} - - -
- - `; -} -``` - ### pairwiseIdentifier Function used by the OP when resolving pairwise ID Token and Userinfo sub claim values. See [Core 1.0](https://openid.net/specs/openid-connect-core-1_0.html#PairwiseAlg) @@ -2872,34 +2919,6 @@ function pkceRequired(ctx, client) { } ``` -### postLogoutSuccessSource - -HTML source rendered when session management feature concludes a logout but there was no `post_logout_redirect_uri` provided by the client. - - -_**default value**_: -```js -async function postLogoutSuccessSource(ctx) { - // @param ctx - koa request context - const { - clientId, clientName, clientUri, initiateLoginUri, logoUri, policyUri, tosUri, - } = ctx.oidc.client || {}; // client is defined if the user chose to stay logged in with the OP - const display = clientName || clientId; - ctx.body = ` - - Sign-out Success - - - -
-

Sign-out Success

-

Your sign-out ${display ? `with ${display}` : ''} was successful.

-
- - `; -} -``` - ### renderError Function used to present errors to the User-Agent diff --git a/lib/actions/discovery.js b/lib/actions/discovery.js index 369cd0fe0..5b0d31224 100644 --- a/lib/actions/discovery.js +++ b/lib/actions/discovery.js @@ -15,7 +15,7 @@ module.exports = function discovery(ctx, next) { claims_parameter_supported: features.claimsParameter.enabled, claims_supported: [...config.claimsSupported], code_challenge_methods_supported: config.pkce.methods, - end_session_endpoint: ctx.oidc.urlFor('end_session'), + end_session_endpoint: features.rpInitiatedLogout.enabled ? ctx.oidc.urlFor('end_session') : undefined, check_session_iframe: features.sessionManagement.enabled ? ctx.oidc.urlFor('check_session') : undefined, grant_types_supported: [...config.grantTypes], id_token_signing_alg_values_supported: config.idTokenSigningAlgValues, diff --git a/lib/actions/end_session.js b/lib/actions/end_session.js index b70f70831..1b270cb70 100644 --- a/lib/actions/end_session.js +++ b/lib/actions/end_session.js @@ -86,7 +86,7 @@ module.exports = { ctx.status = 200; const formHtml = `
`; - await instance(ctx.oidc.provider).configuration('logoutSource')(ctx, formHtml); + await instance(ctx.oidc.provider).configuration('features.rpInitiatedLogout.logoutSource')(ctx, formHtml); } else { await formPost(ctx, action, { xsrf: secret, @@ -274,7 +274,7 @@ module.exports = { } ctx.oidc.entity('Client', client); } - await instance(ctx.oidc.provider).configuration('postLogoutSuccessSource')(ctx); + await instance(ctx.oidc.provider).configuration('features.rpInitiatedLogout.postLogoutSuccessSource')(ctx); }, ], }; diff --git a/lib/helpers/client_schema.js b/lib/helpers/client_schema.js index ea8a22599..bf9d14bc0 100644 --- a/lib/helpers/client_schema.js +++ b/lib/helpers/client_schema.js @@ -93,6 +93,10 @@ module.exports = function getSchema(provider) { } } + if (features.rpInitiatedLogout.enabled) { + RECOGNIZED_METADATA.push('post_logout_redirect_uris'); + } + if (features.backchannelLogout.enabled) { RECOGNIZED_METADATA.push('backchannel_logout_session_required'); RECOGNIZED_METADATA.push('backchannel_logout_uri'); @@ -483,13 +487,15 @@ module.exports = function getSchema(provider) { } postLogoutRedirectUris() { - this.post_logout_redirect_uris.forEach((uri) => { - try { - new url.URL(uri); // eslint-disable-line no-new - } catch (err) { - this.invalidate('post_logout_redirect_uris must only contain uris'); - } - }); + if (this.post_logout_redirect_uris) { + this.post_logout_redirect_uris.forEach((uri) => { + try { + new url.URL(uri); // eslint-disable-line no-new + } catch (err) { + this.invalidate('post_logout_redirect_uris must only contain uris'); + } + }); + } } webMessageUris() { diff --git a/lib/helpers/configuration.js b/lib/helpers/configuration.js index b5a2f80ce..7544b512a 100644 --- a/lib/helpers/configuration.js +++ b/lib/helpers/configuration.js @@ -91,6 +91,7 @@ class Configuration { this.collectGrantTypes(); this.checkSubjectTypes(); this.checkPkceMethods(); + this.checkRpInitiatedLogoutHelpers(); this.checkDependantFeatures(); this.checkDeviceFlow(); this.checkAuthMethods(); @@ -359,6 +360,20 @@ class Configuration { }); } + checkRpInitiatedLogoutHelpers() { + if (this.postLogoutSuccessSource) { + this.postLogoutSuccessSourceDeprecationNotice(); + this.features.rpInitiatedLogout.postLogoutSuccessSource = this.postLogoutSuccessSource; + delete this.postLogoutSuccessSource; + } + + if (this.logoutSource) { + this.logoutSourceDeprecationNotice(); + this.features.rpInitiatedLogout.logoutSource = this.logoutSource; + delete this.logoutSource; + } + } + checkPkceMethods() { if (this.pkceMethods) { this.pkceMethodsDeprecationNotice(); @@ -572,4 +587,16 @@ Configuration.prototype.pkceMethodsDeprecationNotice = deprecate( `pkceMethods is deprecated, use pkce.methods for configuring it instead, see ${docs('pkce')}`, ); +Configuration.prototype.postLogoutSuccessSourceDeprecationNotice = deprecate( + /* istanbul ignore next */ + () => {}, + `postLogoutSuccessSourceDeprecationNotice is deprecated, use features.rpInitiatedLogout.postLogoutSuccessSourceDeprecationNotice for configuring it instead, see ${docs('featuresrpinitiatedlogout')}`, +); + +Configuration.prototype.logoutSourceDeprecationNotice = deprecate( + /* istanbul ignore next */ + () => {}, + `logoutSourceDeprecationNotice is deprecated, use features.rpInitiatedLogout.logoutSourceDeprecationNotice for configuring it instead, see ${docs('featuresrpinitiatedlogout')}`, +); + module.exports = Configuration; diff --git a/lib/helpers/defaults.js b/lib/helpers/defaults.js index bb5b30cc4..b9c932a93 100644 --- a/lib/helpers/defaults.js +++ b/lib/helpers/defaults.js @@ -321,7 +321,7 @@ function extraClientMetadataValidator(key, value, metadata, ctx) { // eslint-dis async function postLogoutSuccessSource(ctx) { // @param ctx - koa request context - shouldChange('postLogoutSuccessSource', 'customize the look of the default post logout success page'); + shouldChange('features.rpInitiatedLogout.postLogoutSuccessSource', 'customize the look of the default post logout success page'); const { clientId, clientName, clientUri, initiateLoginUri, logoUri, policyUri, tosUri, // eslint-disable-line no-unused-vars, max-len } = ctx.oidc.client || {}; // client is defined if the user chose to stay logged in with the OP @@ -349,7 +349,7 @@ async function logoutSource(ctx, form) { // @param ctx - koa request context // @param form - form source (id="op.logoutForm") to be embedded in the page and submitted by // the End-User - shouldChange('logoutSource', 'customize the look of the logout page'); + shouldChange('features.rpInitiatedLogout.logoutSource', 'customize the look of the logout page'); ctx.body = ` @@ -1055,6 +1055,32 @@ function getDefaults() { */ fapiRW: { enabled: false }, + /* + * features.rpInitiatedLogout + * + * title: [RP-Initiated Logout 1.0](https://openid.net/specs/openid-connect-rpinitiated-1_0-01.html) + * + * description: Enables RP-Initiated Logout features + */ + rpInitiatedLogout: { + enabled: true, + + /* + * features.rpInitiatedLogout.postLogoutSuccessSource + * + * description: HTML source rendered when session management feature concludes a logout but there + * was no `post_logout_redirect_uri` provided by the client. + */ + postLogoutSuccessSource, + + /* + * features.rpInitiatedLogout.logoutSource + * + * description: HTML source rendered when session management feature renders a confirmation + * prompt for the User-Agent. + */ + logoutSource, + }, /* * features.frontchannelLogout * @@ -2100,22 +2126,6 @@ function getDefaults() { validator: extraClientMetadataValidator, }, - /* - * postLogoutSuccessSource - * - * description: HTML source rendered when session management feature concludes a logout but there - * was no `post_logout_redirect_uri` provided by the client. - */ - postLogoutSuccessSource, - - /* - * logoutSource - * - * description: HTML source rendered when session management feature renders a confirmation - * prompt for the User-Agent. - */ - logoutSource, - /* * renderError * @@ -2660,6 +2670,8 @@ function getDefaults() { }, pkceMethods: undefined, + postLogoutSuccessSource: undefined, + logoutSource: undefined, }; if (!runtimeSupport.EdDSA) { diff --git a/lib/helpers/features.js b/lib/helpers/features.js index d19c620eb..092ac5ca2 100644 --- a/lib/helpers/features.js +++ b/lib/helpers/features.js @@ -11,6 +11,7 @@ const STABLE = new Set([ 'registrationManagement', 'requestObjects', 'revocation', + 'rpInitiatedLogout', 'userinfo', ]); diff --git a/lib/helpers/initialize_app.js b/lib/helpers/initialize_app.js index 69d871870..a0b7e8859 100644 --- a/lib/helpers/initialize_app.js +++ b/lib/helpers/initialize_app.js @@ -189,10 +189,13 @@ module.exports = function initializeApp() { post('check_session_origin', routes.check_session, error(this, 'check_session_origin.error'), ...checkSession.post); } - get('end_session', routes.end_session, error(this, 'end_session.error'), ...endSession.init); - post('end_session', routes.end_session, error(this, 'end_session.error'), ...endSession.init); post('end_session_confirm', `${routes.end_session}/confirm`, error(this, 'end_session_confirm.error'), ...endSession.confirm); - get('end_session_success', `${routes.end_session}/success`, error(this, 'end_session_success.error'), ...endSession.success); + + if (configuration.features.rpInitiatedLogout.enabled) { + post('end_session', routes.end_session, error(this, 'end_session.error'), ...endSession.init); + get('end_session', routes.end_session, error(this, 'end_session.error'), ...endSession.init); + get('end_session_success', `${routes.end_session}/success`, error(this, 'end_session_success.error'), ...endSession.success); + } if (configuration.features.deviceFlow.enabled) { const deviceAuthorization = getAuthorization(this, 'device_authorization'); diff --git a/test/device_code/device_code.config.js b/test/device_code/device_code.config.js index 607a762fe..e786d0acb 100644 --- a/test/device_code/device_code.config.js +++ b/test/device_code/device_code.config.js @@ -9,6 +9,7 @@ merge(config.features, { claimsParameter: { enabled: true }, requestObjects: { request: false, requestUri: false }, resourceIndicators: { enabled: true }, + rpInitiatedLogout: { enabled: false }, }); config.extraParams = [ diff --git a/test/end_session/end_session.test.js b/test/end_session/end_session.test.js index ff8588e72..e9c3416c8 100644 --- a/test/end_session/end_session.test.js +++ b/test/end_session/end_session.test.js @@ -464,7 +464,7 @@ describe('logout endpoint', () => { describe('GET end_session_success', () => { it('calls the postLogoutSuccessSource helper', function () { - const renderSpy = sinon.spy(i(this.provider).configuration(), 'postLogoutSuccessSource'); + const renderSpy = sinon.spy(i(this.provider).configuration('features.rpInitiatedLogout'), 'postLogoutSuccessSource'); return this.agent.get('/session/end/success') .set('Accept', 'text/html') .expect(200) @@ -476,7 +476,7 @@ describe('logout endpoint', () => { }); it('has the client loaded when present', function () { - const renderSpy = sinon.spy(i(this.provider).configuration(), 'postLogoutSuccessSource'); + const renderSpy = sinon.spy(i(this.provider).configuration('features.rpInitiatedLogout'), 'postLogoutSuccessSource'); return this.agent.get('/session/end/success?client_id=client') .set('Accept', 'text/html') .expect(200) diff --git a/test/interaction/interaction.config.js b/test/interaction/interaction.config.js index 567d9d5ef..d716e3f77 100644 --- a/test/interaction/interaction.config.js +++ b/test/interaction/interaction.config.js @@ -5,7 +5,10 @@ const config = cloneDeep(require('../default.config')); const { Check, Prompt, base } = require('../../lib/helpers/interaction_policy'); config.extraParams = ['triggerCustomFail', 'triggerUnrequestable']; -merge(config.features, { sessionManagement: { enabled: true } }); +merge(config.features, { + sessionManagement: { enabled: true }, + rpInitiatedLogout: { enabled: false }, +}); const policy = base();