Skip to content

Commit 1ccf613

Browse files
Implement OIDC /signout support
1 parent e79824b commit 1ccf613

File tree

5 files changed

+117
-30
lines changed

5 files changed

+117
-30
lines changed

lib/api/accounts/signout.js

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,37 @@
11
module.exports = signout
22

3+
const debug = require('../../debug')
4+
5+
/**
6+
* Handles the /signout API call.
7+
* @param req
8+
* @param res
9+
*/
310
function signout () {
4-
return (req, res, next) => {
11+
return (req, res) => {
12+
const locals = req.app.locals
13+
const ldp = locals.ldp
14+
const userId = req.session.userId
15+
debug.idp(`Signing out user: ${userId}`)
16+
const idToken = req.session.idToken
17+
if (idToken && ldp.auth === 'oidc') {
18+
const issuer = req.session.issuer
19+
const oidcRpClient = locals.oidc
20+
Promise.resolve()
21+
.then(() => {
22+
return oidcRpClient.clientForIssuer(issuer)
23+
})
24+
.then((userOidcClient) => {
25+
return userOidcClient.client.signout(idToken)
26+
})
27+
.catch((err) => {
28+
debug.oidc('Error signing out: ', err)
29+
})
30+
}
531
req.session.userId = ''
632
req.session.identified = false
7-
res.status(200).send()
33+
debug.oidc('signout() finished. Redirecting.')
34+
res.redirect('/signed_out.html')
35+
// res.status(200).send('You have been signed out.')
836
}
937
}

lib/create-app.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,7 @@ function createApp (argv = {}) {
157157
app.post('/api/accounts/signin',
158158
bodyParser.urlencoded({ extended: false }), API.accounts.signin())
159159
app.use('/api/accounts', needsOverwrite)
160+
app.get('/signout', API.accounts.signout())
160161
app.post('/api/accounts/signout', API.accounts.signout())
161162
}
162163

lib/handlers/oidc.js

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,8 @@ function oidcIssuerHeader (req, res, next) {
5858
* creates an OIDC client if necessary, etc.
5959
* After successful authentication, the `req` object has the following
6060
* attributes set:
61-
* - `req.accessToken` (Raw token in encoded string form)
62-
* - `req.accessTokenClaims` (JWT OIDC AccessToken decoded & verified)
61+
* - `req.idToken` (Raw OIDC ID token in encoded string form)
62+
* - `req.refreshToken` (OIDC Refresh Token in encoded string form)
6363
* - `req.oidcClient` (OIDC client *for this particular request*)
6464
* If there is no access token (and thus no authentication), all those values
6565
* above will be null.
@@ -188,31 +188,31 @@ function authCodeFlowCallback (oidcRpClient) {
188188
// local / trusted issuer
189189
issuer = oidcRpClient.trustedClient.client.issuer
190190
}
191-
192191
oidcRpClient.clientForIssuer(issuer)
193-
.then((client) => {
192+
.then((oidcClient) => {
194193
debug.oidc('loadAuthClient: Client initialized')
195194
req.oidcIssuer = issuer
196-
req.oidcClient = client
197-
return client
198-
})
199-
.then(() => {
200-
return req.oidcClient.client.token(tokenOptions)
195+
req.oidcClient = oidcClient
196+
// Send a request to trade the Auth flow code for an ID Token
197+
return oidcClient.client.token(tokenOptions)
201198
})
202199
.then((tokenResult) => {
203200
accessToken = tokenResult.access_token
204-
debug.oidc(tokenResult)
205-
debug.oidc('Verifying token')
206-
return req.oidcClient.verifyToken(req, accessToken)
207-
})
208-
.then(() => {
209-
// Verification of token successful. Load the webid from id token
210-
debug.oidc('Token verified')
211-
let webId = req.accessTokenClaims['sub']
201+
let idToken = tokenResult.id_token
202+
let refreshToken = tokenResult.refresh_token
203+
// debug.oidc(tokenResult)
204+
let webId = tokenResult.id_claims.sub
212205
// req.userInfo = { profile: webId }
213-
req.accessToken = accessToken
206+
req.accessTokenClaims = tokenResult.access_claims
207+
req.session.accessToken = accessToken
208+
req.session.idToken = idToken
209+
req.session.refreshToken = refreshToken
210+
req.session.issuer = issuer
214211
req.session.userId = webId
215212
req.session.identified = true
213+
214+
// Also store the client in the user's session. Used later by signout()
215+
req.session.oidcClient = req.oidcClient
216216
next()
217217
// return oidcRpClient.trustedClient.client.userInfo({ token: accessToken })
218218
})
@@ -237,8 +237,8 @@ function resumeUserFlow (req, res, next) {
237237

238238
if (req.session.returnToUrl) {
239239
let returnToUrl = req.session.returnToUrl
240-
if (req.accessToken) {
241-
returnToUrl += '?access_token=' + req.accessToken
240+
if (req.session.accessToken) {
241+
returnToUrl += '?access_token=' + req.session.accessToken
242242
}
243243
debug.oidc(' Redirecting to ' + returnToUrl)
244244
delete req.session.returnToUrl

lib/oidc-rp-client.js

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,11 @@ module.exports = class OidcRpClient {
3434
* is tied to / registered with a specific OIDC Provider).
3535
* @method authUrl
3636
* @param oidcClient {OIDCExpressClient}
37-
* @param flowType {String} OIDC workflow type, one of 'code' or 'implicit'.
37+
* @param workflow {String} OIDC workflow type, one of 'code' or 'implicit'.
3838
* @return {String} Absolute URL for an OIDC auth call (to start either
3939
* the Authorization Code workflow, or the Implicit workflow).
4040
*/
41-
authUrl (oidcClient, flowType = 'code') {
41+
authUrl (oidcClient, workflow = 'code') {
4242
let authParams = {
4343
endpoint: 'signin',
4444
response_mode: 'query',
@@ -48,9 +48,9 @@ module.exports = class OidcRpClient {
4848
// state: '...', // not doing state for the moment
4949
scope: 'openid profile' // not doing 'openid profile' for the moment
5050
}
51-
if (flowType === 'code') { // Authorization Code workflow
51+
if (workflow === 'code') { // Authorization Code workflow
5252
authParams.response_type = 'code'
53-
} else if (flowType === 'implicit') {
53+
} else if (workflow === 'implicit') {
5454
authParams.response_type = 'id_token token'
5555
authParams.nonce = '123' // TODO: Implement proper nonce generation
5656
}
@@ -60,10 +60,17 @@ module.exports = class OidcRpClient {
6060
return signinUrl
6161
}
6262

63-
authUrlForIssuer (issuer) {
63+
/**
64+
* Returns a constructed `/authorization` URL for a given issuer. Used for
65+
* starting the OIDC workflow.
66+
* @param issuer {String} OIDC Provider URL
67+
* @param workflow {String} OIDC workflow type, one of 'code' or 'implicit'
68+
* @returns {Promise}
69+
*/
70+
authUrlForIssuer (issuer, workflow = 'code') {
6471
return this.clientForIssuer(issuer)
6572
.then((client) => {
66-
return this.authUrl(client, 'code')
73+
return this.authUrl(client, workflow)
6774
})
6875
}
6976

@@ -146,6 +153,7 @@ module.exports = class OidcRpClient {
146153
* @param config.redirect_uri {String} Callback URL invoked by provider
147154
* @param config.client_id {String} Pre-registered trusted client id
148155
* @param config.client_secret {String} Pre-registered trusted client secret
156+
* @param config.post_logout_redirect_uris {Array<String>}
149157
* @return {Promise<OIDCExpressClient>}
150158
*/
151159
ensureTrustedClient (config) {
@@ -169,6 +177,9 @@ module.exports = class OidcRpClient {
169177
return client
170178
})
171179
})
180+
.catch((err) => {
181+
debug.oidc('Error initializing trusted client!', err)
182+
})
172183
}
173184

174185
/**
@@ -180,13 +191,15 @@ module.exports = class OidcRpClient {
180191
* @param config.redirect_uri {String}
181192
* @param [config.client_id] {String} Pre-registered trusted client id
182193
* @param [config.client_secret] {String} Pre-registered trusted client secret
194+
* @param [config.post_logout_redirect_uris] {Array<String>}
183195
* @return {Promise<OIDCExpressClient>} Initialized/registered api client
184196
*/
185197
initClient (config, isTrustedClient = false) {
186198
var oidcExpress = new OIDCExpressClient(config)
187199
// registration spec takes a list of redirect uris. just go with it..
188200
let redirectUris = [ config.redirect_uri ]
189-
var registration = this.registrationConfig(config.issuer, redirectUris)
201+
var registration = this.registrationConfig(config.issuer, redirectUris,
202+
config.post_logout_redirect_uris)
190203
debug.oidc('Registration config: ')
191204
debug.oidc(registration)
192205
debug.oidc('Running client.initProvider()...')
@@ -197,6 +210,10 @@ module.exports = class OidcRpClient {
197210
// Register if you haven't already.
198211
debug.oidc('Registering client')
199212
return oidcExpress.client.register(registration)
213+
} else {
214+
// Already registered.
215+
oidcExpress.client.registration = registration
216+
return oidcExpress
200217
}
201218
})
202219
.then(() => {
@@ -226,9 +243,10 @@ module.exports = class OidcRpClient {
226243
* @param issuer {String} URL of the OIDC Provider / issuer.
227244
* @param redirectUris {Array<String>} List of allowed URIs to which the
228245
* provider will redirect users after login etc.
246+
* @param [postLogoutUris] {Array<String>}
229247
* @return {Object} OIDC Client registration config options
230248
*/
231-
registrationConfig (issuer, redirectUris) {
249+
registrationConfig (issuer, redirectUris, postLogoutUris) {
232250
let clientName = `Solid OIDC Client for ${issuer}`
233251
let config = {
234252
client_name: clientName,
@@ -245,6 +263,9 @@ module.exports = class OidcRpClient {
245263
response_types: ['code', 'id_token token', 'code id_token token'],
246264
scope: 'openid profile'
247265
}
266+
if (postLogoutUris) {
267+
config.post_logout_redirect_uris = postLogoutUris
268+
}
248269
return config
249270
}
250271
}

static/oidc/signed_out.html

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<!doctype html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="utf-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1">
6+
<title>Signed Out</title>
7+
<!-- Bootstrap CSS and Theme for demo purposes -->
8+
<link rel="stylesheet" href="css/bootstrap-3.3.6.min.css">
9+
</head>
10+
<body>
11+
<div class="container">
12+
<h3>You have signed out.</h3>
13+
</div>
14+
<div class="container">
15+
<form method="post" action="/api/accounts/signin">
16+
<div class="form-group">
17+
<label for="webid">WebID or server URL:</label>
18+
<input type="text" class="form-control" name="webid" id="webid" placeholder="databox.me" />
19+
<input type="hidden" name="returnToUrl" id="returnToUrl" value="" />
20+
</div>
21+
<button type="submit" class="btn btn-primary"
22+
id="login">Sign In Again</button>
23+
</form>
24+
</div>
25+
<script>
26+
document.addEventListener('DOMContentLoaded', function () { init() })
27+
28+
function init () {
29+
var query = window.location.search
30+
query = query.replace('?', '')
31+
var returnToUrl = query.split('=')[1]
32+
console.log(returnToUrl)
33+
document.getElementById('returnToUrl').value = returnToUrl
34+
}
35+
</script>
36+
</body>
37+
</html>

0 commit comments

Comments
 (0)