Skip to content

Commit

Permalink
feat: add RP-Initiated Logout URL helper
Browse files Browse the repository at this point in the history
closes #116
  • Loading branch information
panva committed Sep 16, 2018
1 parent 95fae3d commit 7c2e030
Show file tree
Hide file tree
Showing 4 changed files with 138 additions and 2 deletions.
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ openid-client.
- [RFC7662 - OAuth 2.0 Token introspection][feature-introspection]
- Client Authenticated request to token introspection

Updates to features defined in draft or experimental specifications are released as MINOR library
versions, if you utilize these consider using the tilde ~ operator in your package.json since
breaking changes may be introduced as part of these specification updates.

## Certification
[<img width="184" height="96" align="right" src="https://cdn.rawgit.com/panva/node-openid-client/38cf016b/OpenID_Certified.png" alt="OpenID Certification">][openid-certified-link]
Expand Down Expand Up @@ -254,6 +257,18 @@ userinfo also handles (as long as you have the proper metadata configured) respo
- signed and encrypted (nested JWT)
- just encrypted

### Getting RP-Initiated Logout url

Note: Only usable with issuer's supporting OpenID Connect Session Management 1.0

```js
client.endSessionUrl({
post_logout_redirect_uri: '...', // OPTIONAL, defaults to client.post_logout_redirect_uris[0] if there's only one
state: '...', // RECOMMENDED
id_token_hint: '...', // OPTIONAL, accepts the string value or tokenSet with id_token
}); // => String (URL)
```

### Fetching Distributed Claims
```js
let claims = {
Expand Down
40 changes: 40 additions & 0 deletions lib/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,8 @@ function authorizationParams(params) {
params
);

// TODO: default redirect_uris if there's one

forEach(authParams, (value, key) => {
if (value === null || value === undefined) {
delete authParams[key];
Expand Down Expand Up @@ -267,6 +269,44 @@ class Client {
</html>`;
}

/**
* @name endSessionUrl
* @api public
*/
endSessionUrl(params = {}) {
assertIssuerConfiguration(this.issuer, 'end_session_endpoint');

const {
0: postLogout,
length,
} = this.post_logout_redirect_uris || [];

const {
post_logout_redirect_uri = length === 1 ? postLogout : undefined,
...rest
} = params;

let hint = params.id_token_hint;

if (hint instanceof TokenSet) {
assert(hint.id_token, 'id_token not present in TokenSet');
hint = hint.id_token;
}

const target = url.parse(this.issuer.end_session_endpoint, true);
target.search = null;
target.query = Object.assign(rest, target.query, {
post_logout_redirect_uri,
id_token_hint: hint,
});
forEach(target.query, (value, key) => {
if (value === null || value === undefined) {
delete target.query[key];
}
});
return url.format(target);
}

/**
* @name callbackParams
* @api public
Expand Down
2 changes: 1 addition & 1 deletion lib/passport_strategy.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ function OpenIDConnectStrategy({
this.name = url.parse(client.issuer.issuer).hostname;

if (!params.response_type) params.response_type = _.get(client, 'response_types[0]', 'code');
if (!params.redirect_uri) params.redirect_uri = _.get(client, 'redirect_uris[0]');
if (!params.redirect_uri) params.redirect_uri = _.get(client, 'redirect_uris[0]'); // TODO: only default if there's one
if (!params.scope) params.scope = 'openid';
}

Expand Down
83 changes: 82 additions & 1 deletion test/client/client_instance.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ const encode = object => base64url.encode(JSON.stringify(object));
});
});

it('keeps origin query parameters', function () {
it('keeps original query parameters', function () {
expect(url.parse(this.clientWithQuery.authorizationUrl({
redirect_uri: 'https://rp.example.com/cb',
}), true).query).to.eql({
Expand Down Expand Up @@ -120,6 +120,87 @@ const encode = object => base64url.encode(JSON.stringify(object));
});
});

describe('#endSessionUrl', function () {
before(function () {
const issuer = new Issuer({
end_session_endpoint: 'https://op.example.com/session/end',
});
this.client = new issuer.Client({
client_id: 'identifier',
});
this.clientWithUris = new issuer.Client({
post_logout_redirect_uris: ['https://rp.example.com/logout/cb'],
});

const issuerWithQuery = new Issuer({
end_session_endpoint: 'https://op.example.com/session/end?foo=bar',
});
this.clientWithQuery = new issuerWithQuery.Client({
client_id: 'identifier',
});

const issuerWithoutMeta = new Issuer({
// end_session_endpoint: 'https://op.example.com/session/end?foo=bar',
});
this.clientWithoutMeta = new issuerWithoutMeta.Client({
client_id: 'identifier',
});
});

it("throws if the issuer doesn't have end_session_endpoint configured", function () {
expect(() => {
this.clientWithoutMeta.endSessionUrl();
}).to.throw('end_session_endpoint must be configured on the issuer');
});

it('returns the end_session_endpoint only if nothing is passed', function () {
expect(this.client.endSessionUrl()).to.eql('https://op.example.com/session/end');
expect(this.clientWithQuery.endSessionUrl()).to.eql('https://op.example.com/session/end?foo=bar');
});

it('defaults the post_logout_redirect_uri if client has some', function () {
expect(url.parse(this.clientWithUris.endSessionUrl(), true).query).to.eql({
post_logout_redirect_uri: 'https://rp.example.com/logout/cb',
});
});

it('takes a TokenSet too', function () {
const hint = new TokenSet({
id_token: 'eyJhbGciOiJub25lIn0.eyJzdWIiOiJzdWJqZWN0In0.',
refresh_token: 'bar',
access_token: 'tokenValue',
});
expect(url.parse(this.client.endSessionUrl({
id_token_hint: hint,
}), true).query).to.eql({
id_token_hint: 'eyJhbGciOiJub25lIn0.eyJzdWIiOiJzdWJqZWN0In0.',
});
});

it('allows for recommended and optional query params to be passed in', function () {
expect(url.parse(this.client.endSessionUrl({
post_logout_redirect_uri: 'https://rp.example.com/logout/cb',
state: 'foo',
id_token_hint: 'idtoken',
}), true).query).to.eql({
post_logout_redirect_uri: 'https://rp.example.com/logout/cb',
state: 'foo',
id_token_hint: 'idtoken',
});
expect(url.parse(this.clientWithQuery.endSessionUrl({
post_logout_redirect_uri: 'https://rp.example.com/logout/cb',
state: 'foo',
id_token_hint: 'idtoken',
foo: 'this will be ignored',
}), true).query).to.eql({
post_logout_redirect_uri: 'https://rp.example.com/logout/cb',
state: 'foo',
foo: 'bar',
id_token_hint: 'idtoken',
});
});
});

describe('#authorizationPost', function () {
const REGEXP = /name="(.+)" value="(.+)"/g;

Expand Down

0 comments on commit 7c2e030

Please sign in to comment.