-
Notifications
You must be signed in to change notification settings - Fork 1
fix: actually invalidate the session on OAuth logout #211
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
base: main
Are you sure you want to change the base?
Changes from all commits
029ecb8
3681a49
293fac7
7fad2e9
c1e07fb
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -532,21 +532,44 @@ export async function handleCallback( | |
| } | ||
|
|
||
| /** | ||
| * Clear OAuth session data and log out the user | ||
| * Shared function for explicit logout and automatic logout on token expiration | ||
| * Clear OAuth session data and log out the user. | ||
| * Shared by explicit logout and automatic logout on token expiration. | ||
| * | ||
| * Deletes the session record from the hdb_session table, completely removing it | ||
| * rather than just clearing the user field. This ensures no orphaned sessions remain. | ||
| * `request.session` is a shallow copy of the `hdb_session` record exposing only | ||
| * `.update` (a full-replace `put` keyed on the session id) — it has NO `.delete`, | ||
| * and mutating the copy in memory never persists. So we INVALIDATE by persisting | ||
| * a null-user record via `.update`, mirroring Harper's own `logout()`: on the | ||
| * next request `session.user` is null, so the bearer resolves to no user. The | ||
| * previous code's `session.delete` branch never ran (no such method) and its | ||
| * in-memory fallback left the stored `hdb_session` record fully valid — a | ||
| * captured cookie (or an upstream-revoked account) kept authenticating. | ||
| */ | ||
| export async function clearOAuthSession(session: any, logger?: Logger): Promise<void> { | ||
| if (!session) return; | ||
|
|
||
| // Delete the session record from the hdb_session table | ||
| // This completely removes the session on logout, rather than just nulling the user field | ||
| if (typeof session.delete === 'function') { | ||
| await session.delete(session.id); | ||
| // Only persist an invalidation when there is an EXISTING session to invalidate. | ||
| // Harper defines `.update` on every request — including anonymous, cookie-less | ||
| // ones — and calling it mints a fresh hdb_session row (new UUID + Set-Cookie), | ||
| // with no expiry when `authentication.cookieExpires` is unset. Guarding on | ||
| // `session.id` stops an unauthenticated POST /oauth/logout from spamming | ||
| // non-expiring rows. | ||
| // | ||
| // `session.id` is the right signal here because every caller of this function | ||
| // (logout, validateAndRefreshSession, the provider-gone middleware) runs on a | ||
| // session LOADED FROM A COOKIE, which carries its id. Known limitation, not | ||
| // reachable via any OAuth flow today: a session created id-less earlier in the | ||
| // SAME request via a separate `update({...})` payload wouldn't expose an id | ||
| // here (Harper mints it onto the payload, not back onto request.session), so | ||
| // this would no-op. OAuth never creates-then-clears in one request. | ||
| if (session.id && typeof session.update === 'function') { | ||
| // Match Harper's own logout(): a full-replace put of `{ user: null }` clears | ||
| // the identity (unauthenticated on the next request) AND drops the oauth | ||
| // tokens — leaving them absent, not `null`, which keeps the `Session` type | ||
| // and the downstream `session.oauth === undefined` checks honest. | ||
| await session.update({ user: null }); | ||
|
Comment on lines
+564
to
+569
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Blocker — production path leaves
|
||
| } else { | ||
| // Fallback for sessions without delete method - clear in-memory | ||
| // No existing session / no persistence (anonymous logout, non-session | ||
| // transport, tests): clear in memory only. | ||
| session.user = null; | ||
| delete session.oauth; | ||
| delete session.oauthUser; | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔴 Blocker — In-memory session state inconsistency in production path
The production path calls update() but doesn't clear the in-memory session fields. This causes requireAuth: false resources to see stale identity/tokens for the remainder of the current request. Move the in-memory clearing (session.user = null, delete session.oauth/oauthUser) outside the else block.