-
Notifications
You must be signed in to change notification settings - Fork 141
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
OIDC Back-Channel Logout #484
Merged
Merged
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,103 @@ | ||
const { assert } = require('chai'); | ||
const puppeteer = require('puppeteer'); | ||
const request = require('request-promise-native'); | ||
const provider = require('./fixture/oidc-provider'); | ||
const { | ||
baseUrl, | ||
start, | ||
runExample, | ||
stubEnv, | ||
checkContext, | ||
goto, | ||
login, | ||
} = require('./fixture/helpers'); | ||
|
||
describe('back-channel logout', async () => { | ||
let authServer; | ||
let appServer; | ||
let browser; | ||
|
||
beforeEach(async () => { | ||
stubEnv(); | ||
authServer = await start(provider, 3001); | ||
}); | ||
|
||
afterEach(async () => { | ||
authServer.close(); | ||
appServer.close(); | ||
await browser.close(); | ||
}); | ||
|
||
const runTest = async (example) => { | ||
appServer = await runExample(example); | ||
browser = await puppeteer.launch({ | ||
args: ['no-sandbox', 'disable-setuid-sandbox'], | ||
}); | ||
const page = await browser.newPage(); | ||
await goto(baseUrl, page); | ||
assert.match(page.url(), /http:\/\/localhost:300/); | ||
await Promise.all([page.click('a'), page.waitForNavigation()]); | ||
await login('username', 'password', page); | ||
assert.equal( | ||
page.url(), | ||
`${baseUrl}/`, | ||
'User is returned to the original page' | ||
); | ||
const loggedInCookies = await page.cookies('http://localhost:3000'); | ||
assert.ok(loggedInCookies.find(({ name }) => name === 'appSession')); | ||
|
||
const response = await checkContext(await page.cookies()); | ||
assert.isOk(response.isAuthenticated); | ||
|
||
await goto(`${baseUrl}/logout-token`, page); | ||
|
||
await page.waitForSelector('pre'); | ||
const element = await page.$('pre'); | ||
const curl = await page.evaluate((el) => el.textContent, element); | ||
const [, logoutToken] = curl.match(/logout_token=([^"]+)/); | ||
const res = await request.post('http://localhost:3000/backchannel-logout', { | ||
form: { | ||
logout_token: logoutToken, | ||
}, | ||
resolveWithFullResponse: true, | ||
}); | ||
assert.equal(res.statusCode, 204); | ||
|
||
await goto(baseUrl, page); | ||
const loggedOutCookies = await page.cookies('http://localhost:3000'); | ||
assert.notOk(loggedOutCookies.find(({ name }) => name === 'appSession')); | ||
}; | ||
|
||
it('should logout via back-channel logout', () => | ||
runTest('backchannel-logout')); | ||
|
||
it('should not logout sub via back-channel logout if user logs in after', async () => { | ||
await runTest('backchannel-logout'); | ||
|
||
await browser.close(); | ||
browser = await puppeteer.launch({ | ||
args: ['no-sandbox', 'disable-setuid-sandbox'], | ||
}); | ||
const page = await browser.newPage(); | ||
await goto(baseUrl, page); | ||
assert.match(page.url(), /http:\/\/localhost:300/); | ||
await Promise.all([page.click('a'), page.waitForNavigation()]); | ||
await login('username', 'password', page); | ||
assert.equal( | ||
page.url(), | ||
`${baseUrl}/`, | ||
'User is returned to the original page' | ||
); | ||
|
||
const loggedInCookies = await page.cookies('http://localhost:3000'); | ||
assert.ok(loggedInCookies.find(({ name }) => name === 'appSession')); | ||
const response = await checkContext(await page.cookies()); | ||
assert.isOk(response.isAuthenticated); | ||
}); | ||
|
||
it('should logout via back-channel logout with custom implementation genid', () => | ||
runTest('backchannel-logout-custom-genid')); | ||
|
||
it('should logout via back-channel logout with custom implementation query store', () => | ||
runTest('backchannel-logout-custom-query-store')); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
const { JWK } = require('jose'); | ||
|
||
const key = JWK.generateSync('RSA', 2048, { | ||
alg: 'RS256', | ||
kid: 'key-1', | ||
use: 'sig', | ||
}); | ||
|
||
module.exports.privateJWK = key.toJWK(true); | ||
module.exports.publicJWK = key.toJWK(); | ||
module.exports.privatePEM = key.toPEM(true); | ||
module.exports.publicPEM = key.toPEM(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
const { promisify } = require('util'); | ||
const crypto = require('crypto'); | ||
const express = require('express'); | ||
const { auth, requiresAuth } = require('../'); | ||
const { logoutTokenTester } = require('../end-to-end/fixture/helpers'); | ||
|
||
// This custom implementation uses a sessions with an id that matches the | ||
// Identity Provider's session id "sid" (by using the "genid" config). | ||
// When the SDK receives a logout token, it can identify the session that needs | ||
// to be destroyed by the logout token's "sid". | ||
|
||
const MemoryStore = require('memorystore')(auth); | ||
|
||
const app = express(); | ||
|
||
const store = new MemoryStore(); | ||
const destroy = promisify(store.destroy).bind(store); | ||
|
||
const onLogoutToken = async (token) => { | ||
const { sid } = token; | ||
// Delete the session - no need to store a logout token. | ||
await destroy(sid); | ||
}; | ||
|
||
app.use( | ||
auth({ | ||
clientID: 'backchannel-logout-client', | ||
authRequired: false, | ||
idpLogout: true, | ||
backchannelLogout: { | ||
onLogoutToken, | ||
isLoggedOut: false, | ||
onLogin: false, | ||
}, | ||
session: { | ||
store, | ||
// If you're using a custom `genid` you should sign the session store cookie | ||
// to ensure it is a cryptographically secure random string and not guessable. | ||
signSessionStoreCookie: true, | ||
genid(req) { | ||
if (req.oidc && req.oidc.isAuthenticated()) { | ||
const { sid } = req.oidc.idTokenClaims; | ||
// Note this must be unique and a cryptographically secure random value. | ||
return sid; | ||
} else { | ||
// Anonymous user sessions (like checkout baskets) | ||
return crypto.randomBytes(16).toString('hex'); | ||
} | ||
}, | ||
}, | ||
}) | ||
); | ||
|
||
app.get('/', async (req, res) => { | ||
if (req.oidc.isAuthenticated()) { | ||
res.send(`hello ${req.oidc.user.sub} <a href="/logout">logout</a>`); | ||
} else { | ||
res.send('<a href="/login">login</a>'); | ||
} | ||
}); | ||
|
||
// For testing purposes only | ||
app.get( | ||
'/logout-token', | ||
requiresAuth(), | ||
logoutTokenTester('backchannel-logout-client', true) | ||
); | ||
|
||
module.exports = app; |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Check failure
Code scanning / CodeQL
Missing rate limiting