Skip to content

Commit

Permalink
feat: added postLogoutSuccessSource helper for logouts without redirects
Browse files Browse the repository at this point in the history
BREAKING CHANGE: `postLogoutRedirectUri` configuration option is removed
in favour of `postLogoutSuccessSource`. This is used to render a success
page out of the box rather then redirecting nowhere.
  • Loading branch information
panva committed Apr 24, 2019
1 parent e5ac735 commit a979af8
Show file tree
Hide file tree
Showing 7 changed files with 136 additions and 25 deletions.
31 changes: 24 additions & 7 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ If you or your business use oidc-provider, please consider becoming a [Patron][s
- [logoutSource](#logoutsource)
- [pairwiseIdentifier](#pairwiseidentifier)
- [pkceMethods](#pkcemethods)
- [postLogoutRedirectUri](#postlogoutredirecturi)
- [postLogoutSuccessSource](#postlogoutsuccesssource)
- [renderError](#rendererror)
- [responseTypes](#responsetypes)
- [revocationEndpointAuthMethods](#revocationendpointauthmethods)
Expand Down Expand Up @@ -417,6 +417,7 @@ provider.use(async (ctx, next) => {
* `discovery`
* `end_session`
* `end_session_confirm`
* `end_session_success`
* `introspection`
* `registration`
* `resume`
Expand Down Expand Up @@ -871,7 +872,7 @@ head>
ody>
<div>
<h1>Sign-in Success</h1>
<p>Your login ${clientName ? `with ${clientName}` : ''} was successful, you can now close this page.</p>
<p>Your sign-in ${clientName ? `with ${clientName}` : ''} was successful, you can now close this page.</p>
</div>
body>
html>`;
Expand Down Expand Up @@ -2294,7 +2295,7 @@ async issueRefreshToken(ctx, client, code) {

### logoutSource

HTML source rendered when when session management feature renders a confirmation prompt for the User-Agent.
HTML source rendered when session management feature renders a confirmation prompt for the User-Agent.


_**default value**_:
Expand Down Expand Up @@ -2366,15 +2367,31 @@ _**default value**_:
]
```

### postLogoutRedirectUri
### postLogoutSuccessSource

URL to which the OP redirects the User-Agent when no post_logout_redirect_uri and id_token_hint is provided by the RP
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 postLogoutRedirectUri(ctx) {
return ctx.origin;
async 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 = `<!DOCTYPE html>
<head>
<title>Sign-out Success</title>
<style>/* css and html classes omitted for brevity, see lib/helpers/defaults.js */</style>
</head>
<body>
<div>
<h1>Sign-out Success</h1>
<p>Your sign-out ${display ? `with ${display}` : ''} was successful.</p>
</div>
</body>
</html>`;
}
```

Expand Down
24 changes: 21 additions & 3 deletions lib/actions/end_session.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,7 @@ module.exports = {
secret,
clientId: ctx.oidc.client ? ctx.oidc.client.clientId : undefined,
state: ctx.oidc.params.state,
postLogoutRedirectUri: ctx.oidc.params.post_logout_redirect_uri
|| await instance(ctx.oidc.provider).configuration('postLogoutRedirectUri')(ctx),
postLogoutRedirectUri: ctx.oidc.params.post_logout_redirect_uri || ctx.oidc.urlFor('end_session_success'),
};

ctx.type = 'html';
Expand Down Expand Up @@ -211,7 +210,11 @@ module.exports = {

const uri = redirectUri(
state.postLogoutRedirectUri,
state.state != null ? { state: state.state } : undefined, // != intended
Object.assign(
{},
state.state != null ? { state: state.state } : undefined, // != intended
!params.logout && state.clientId ? { client_id: state.clientId } : undefined,
),
);

ctx.oidc.provider.emit('end_session.success', ctx);
Expand All @@ -228,4 +231,19 @@ module.exports = {
await next();
},
],

success: [
noCache,
paramsMiddleware.bind(undefined, new Set(['client_id'])),
async function postLogoutSuccess(ctx) {
if (ctx.oidc.params.client_id) {
const client = await ctx.oidc.provider.Client.find(ctx.oidc.params.client_id);
if (!client) {
throw new InvalidClient('client not found');
}
ctx.oidc.entity('Client', client);
}
await instance(ctx.oidc.provider).configuration('postLogoutSuccessSource')(ctx);
},
],
};
1 change: 0 additions & 1 deletion lib/helpers/configuration.js
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,6 @@ module.exports = class Configuration {
// // this.clockTolerance
// // this.discovery
// // this.issueRefreshToken
// // this.postLogoutRedirectUri
// // this.logoutSource
// // this.renderError
// // this.interactionUrl
Expand Down
37 changes: 29 additions & 8 deletions lib/helpers/defaults.js
Original file line number Diff line number Diff line change
Expand Up @@ -606,7 +606,7 @@ const DEFAULTS = {
<body>
<div class="container">
<h1>Sign-in Success</h1>
<p>Your login ${clientName ? `with ${clientName}` : ''} was successful, you can now close this page.</p>
<p>Your sign-in ${clientName ? `with ${clientName}` : ''} was successful, you can now close this page.</p>
</div>
</body>
</html>`;
Expand Down Expand Up @@ -1503,21 +1503,42 @@ const DEFAULTS = {
},

/*
* postLogoutRedirectUri
* postLogoutSuccessSource
*
* description: URL to which the OP redirects the User-Agent when no post_logout_redirect_uri
* and id_token_hint is provided by the RP
* description: HTML source rendered when session management feature concludes a logout but there
* was no `post_logout_redirect_uri` provided by the client.
*/
async postLogoutRedirectUri(ctx) { // eslint-disable-line no-unused-vars
shouldChange('postLogoutRedirectUri', 'specify where to redirect the user after logout without post_logout_redirect_uri specified');
return ctx.origin;
async postLogoutSuccessSource(ctx) {
// @param ctx - koa request context
shouldChange('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
const display = clientName || clientId;
ctx.body = `<!DOCTYPE html>
<head>
<meta charset="utf-8">
<title>Sign-out Success</title>
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta http-equiv="x-ua-compatible" content="ie=edge">
<style>
@import url(https://fonts.googleapis.com/css?family=Roboto:400,100);h1,h1+p{font-weight:100;text-align:center}body{font-family:Roboto,sans-serif;margin-top:25px;margin-bottom:25px}.container{padding:0 40px 10px;width:274px;background-color:#F7F7F7;margin:0 auto 10px;border-radius:2px;box-shadow:0 2px 2px rgba(0,0,0,.3);overflow:hidden}h1{font-size:2.3em}
</style>
</head>
<body>
<div class="container">
<h1>Sign-out Success</h1>
<p>Your sign-out ${display ? `with ${display}` : ''} was successful.</p>
</div>
</body>
</html>`;
},


/*
* logoutSource
*
* description: HTML source rendered when when session management feature renders a confirmation
* description: HTML source rendered when session management feature renders a confirmation
* prompt for the User-Agent.
*/
async logoutSource(ctx, form) {
Expand Down
1 change: 1 addition & 0 deletions lib/helpers/initialize_app.js
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,7 @@ module.exports = function initializeApp() {
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.deviceFlow.enabled) {
const deviceAuthorization = getAuthorization(this, 'device_authorization');
Expand Down
4 changes: 3 additions & 1 deletion test/end_session/end_session.config.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
const config = require('../default.config');
const { cloneDeep } = require('lodash');

const config = cloneDeep(require('../default.config'));

module.exports = {
config,
Expand Down
63 changes: 58 additions & 5 deletions test/end_session/end_session.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ const { expect } = require('chai');

const bootstrap = require('../test_helper');
const JWT = require('../../lib/helpers/jwt');
const { InvalidRequest } = require('../../lib/helpers/errors');
const { InvalidClient, InvalidRequest } = require('../../lib/helpers/errors');

const route = '/session/end';

Expand Down Expand Up @@ -93,14 +93,14 @@ describe('logout endpoint', () => {
});
});

it('can omit the post_logout_redirect_uri and uses the provider one', function () {
it('can omit the post_logout_redirect_uri and uses the default one', function () {
const params = { id_token_hint: this.idToken };

return this.wrap({ route, verb, params })
.expect(200)
.expect(() => {
const { state: { postLogoutRedirectUri } } = this.getSession();
expect(postLogoutRedirectUri).to.equal(this.provider.issuer);
expect(postLogoutRedirectUri).to.equal(`${this.provider.issuer}/session/end/success`);
});
});
});
Expand Down Expand Up @@ -311,10 +311,11 @@ describe('logout endpoint', () => {
.send({ xsrf: '123', logout: 'yes' })
.type('form')
.expect(302)
.expect(() => {
.expect((response) => {
expect(adapter.destroy.called).to.be.true;
expect(adapter.upsert.called).not.to.be.true;
expect(adapter.destroy.withArgs(sessionId).calledOnce).to.be.true;
expect(parseUrl(response.headers.location, true).query).not.to.have.property('client_id');
});
});

Expand All @@ -331,12 +332,13 @@ describe('logout endpoint', () => {
.send({ xsrf: '123' })
.type('form')
.expect(302)
.expect(() => {
.expect((response) => {
session = this.getSession();
expect(session.authorizations.client).to.be.undefined;
expect(session.state).to.be.undefined;
expect(this.getSessionId()).not.to.eql(oldId);
expect(adapter.destroy.calledOnceWith(oldId)).to.be.true;
expect(parseUrl(response.headers.location, true).query.client_id).to.eql('client');
});
});

Expand Down Expand Up @@ -374,4 +376,55 @@ describe('logout endpoint', () => {
.expect(302);
});
});

describe('GET end_session_success', () => {
it('calls the postLogoutSuccessSource helper', function () {
const renderSpy = sinon.spy(i(this.provider).configuration(), 'postLogoutSuccessSource');
return this.agent.get('/session/end/success')
.set('Accept', 'text/html')
.expect(() => {
renderSpy.restore();
})
.expect(200)
.expect(() => {
expect(renderSpy.calledOnce).to.be.true;
const [ctx] = renderSpy.args[0];
expect(ctx.oidc.client).to.be.undefined;
});
});

it('has the client loaded when present', function () {
const renderSpy = sinon.spy(i(this.provider).configuration(), 'postLogoutSuccessSource');
return this.agent.get('/session/end/success?client_id=client')
.set('Accept', 'text/html')
.expect(() => {
renderSpy.restore();
})
.expect(200)
.expect(() => {
expect(renderSpy.calledOnce).to.be.true;
const [ctx] = renderSpy.args[0];
expect(ctx.oidc.client).to.be.ok;
});
});

it('throws when the client is not found', function () {
const emitSpy = sinon.spy();
const renderSpy = sinon.spy(i(this.provider).configuration(), 'renderError');
this.provider.once('end_session_success.error', emitSpy);
return this.agent.get('/session/end/success?client_id=foobar')
.set('Accept', 'text/html')
.expect(() => {
renderSpy.restore();
})
.expect(400)
.expect(() => {
expect(emitSpy.calledOnce).to.be.true;
expect(renderSpy.calledOnce).to.be.true;
const renderArgs = renderSpy.args[0];
expect(renderArgs[1]).to.have.property('error', 'invalid_client');
expect(renderArgs[2]).to.be.an.instanceof(InvalidClient);
});
});
});
});

0 comments on commit a979af8

Please sign in to comment.