Skip to content

Commit c1b85da

Browse files
committed
fix(authn): harden OIDC logout cookie cleanup and support solid logout aliases
add robust session/cookie cleanup on goodbye route in webid-oidc.mjs support .well-known and /solid logout route aliases before redirecting to goodbye in webid-oidc.mjs include /solid/logout paths in logout request detection in create-app.mjs
1 parent 83d5630 commit c1b85da

2 files changed

Lines changed: 93 additions & 16 deletions

File tree

lib/api/authn/webid-oidc.mjs

Lines changed: 91 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import bodyParserPkg from 'body-parser'
88
import { fromServerConfig } from '../../models/oidc-manager.mjs'
99
import { LoginRequest } from '../../requests/login-request.mjs'
1010
import { SharingRequest } from '../../requests/sharing-request.mjs'
11+
import debug from '../../debug.mjs'
1112

1213
import restrictToTopDomain from '../../handlers/restrict-to-top-domain.mjs'
1314

@@ -21,6 +22,84 @@ const { urlencoded } = bodyParserPkg
2122
const bodyParser = urlencoded({ extended: false })
2223
const { AuthCallbackRequest } = oidcAuthManager.handlers
2324

25+
function oidcCookieNames (req) {
26+
const provider = req.app?.locals?.oidc?.provider
27+
if (!provider || typeof provider.configuration !== 'function') {
28+
return []
29+
}
30+
31+
const cookieNames = provider.configuration('cookies')?.names || {}
32+
const baseNames = Object.values(cookieNames).filter(Boolean)
33+
const expandedNames = baseNames.flatMap(name => [
34+
name,
35+
`${name}.sig`,
36+
`${name}.legacy`,
37+
`${name}.legacy.sig`
38+
])
39+
40+
return Array.from(new Set(expandedNames))
41+
}
42+
43+
function cookieNamesFromRequest (req) {
44+
const cookieHeader = req.headers?.cookie
45+
if (!cookieHeader) {
46+
return []
47+
}
48+
49+
return cookieHeader
50+
.split(';')
51+
.map(fragment => fragment.trim())
52+
.filter(Boolean)
53+
.map(fragment => fragment.split('=')[0].trim())
54+
.filter(Boolean)
55+
}
56+
57+
function clearAuthCookies (res, domain, cookieNames) {
58+
const cookiePaths = ['/', '/.oidc']
59+
const allCookieNames = Array.from(new Set([
60+
'nssidp.sid',
61+
'nssidp.sid.sig',
62+
'nssidp.sid.legacy',
63+
'nssidp.sid.legacy.sig',
64+
...(cookieNames || [])
65+
]))
66+
67+
for (const path of cookiePaths) {
68+
const noDomainOptions = { path }
69+
for (const name of allCookieNames) {
70+
res.clearCookie(name, noDomainOptions)
71+
}
72+
73+
if (domain) {
74+
const domainOptions = { domain, path }
75+
for (const name of allCookieNames) {
76+
res.clearCookie(name, domainOptions)
77+
}
78+
}
79+
}
80+
}
81+
82+
function renderGoodbyeAndClearSession (req, res, next) {
83+
const domain = req.session?.cookie?.domain
84+
const providerCookieNames = oidcCookieNames(req)
85+
const incomingCookieNames = cookieNamesFromRequest(req)
86+
const cookiesToClear = Array.from(new Set([...providerCookieNames, ...incomingCookieNames]))
87+
88+
if (!req.session) {
89+
clearAuthCookies(res, domain, cookiesToClear)
90+
return res.render('auth/goodbye')
91+
}
92+
93+
req.session.destroy((err) => {
94+
if (err) {
95+
return next(err)
96+
}
97+
98+
clearAuthCookies(res, domain, cookiesToClear)
99+
res.render('auth/goodbye')
100+
})
101+
}
102+
24103
/**
25104
* Sets up OIDC authentication for the given app.
26105
*
@@ -102,25 +181,21 @@ export function middleware (oidc) {
102181
router.get('/account/password/change', restrictToTopDomain, PasswordChangeRequest.get)
103182
router.post('/account/password/change', restrictToTopDomain, bodyParser, PasswordChangeRequest.post)
104183

105-
router.get(['/.well-known/solid/logout', '/.well-known/solid/logout/'], (req, res) => res.redirect('/logout'))
106-
107-
router.get('/goodbye', (req, res, next) => {
108-
if (!req.session) {
109-
return res.render('auth/goodbye')
110-
}
111-
112-
const domain = req.session.cookie && req.session.cookie.domain
113-
req.session.destroy((err) => {
114-
if (err) {
115-
return next(err)
116-
}
184+
router.get([
185+
'/.well-known/solid/logout',
186+
'/.well-known/solid/logout/',
187+
'/solid/logout',
188+
'/solid/logout/'
189+
], (req, res) => {
190+
res.redirect('/goodbye')
191+
})
117192

118-
const cookieOptions = domain ? { domain, path: '/' } : { path: '/' }
119-
res.clearCookie('nssidp.sid', cookieOptions)
120-
res.render('auth/goodbye')
121-
})
193+
router.get(['/logout', '/logout/'], (req, res) => {
194+
res.redirect('/goodbye')
122195
})
123196

197+
router.get('/goodbye', renderGoodbyeAndClearSession)
198+
124199
// The relying party callback is called at the end of the OIDC signin process
125200
router.get('/api/oidc/rp/:issuer_id', AuthCallbackRequest.get)
126201

lib/create-app.mjs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -323,6 +323,8 @@ function isLogoutRequest (req) {
323323
// this code should live in the OIDC module
324324
return req.path === '/logout' ||
325325
req.path === '/goodbye' ||
326+
req.path === '/solid/logout' ||
327+
req.path === '/solid/logout/' ||
326328
req.path === '/.well-known/solid/logout' ||
327329
req.path === '/.well-known/solid/logout/'
328330
}

0 commit comments

Comments
 (0)