Skip to content

Commit 8292882

Browse files
authored
fix(web): propagate session invalidation to /api/auth/session (#1219)
1 parent 3851c66 commit 8292882

2 files changed

Lines changed: 59 additions & 36 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1111
- Fixed issue where repo permissions could go stale when authentication or token refresh related errors occured. [#1215](https://github.com/sourcebot-dev/sourcebot/pull/1215)
1212
- [EE] Fixed issue where repo permissions could go stale when an upstream endpoint returned HTTP 410 Gone (e.g. Bitbucket Cloud's CHANGE-2770). [#1216](https://github.com/sourcebot-dev/sourcebot/pull/1216)
1313
- [EE] Fixed Bitbucket Cloud account-driven permission sync after Atlassian's CHANGE-2770 removed `GET /2.0/user/permissions/repositories`. [#1217](https://github.com/sourcebot-dev/sourcebot/pull/1217)
14-
14+
- Fixed issue where session invalidation (signout, user deletion, removal from org) was not reflected by `/api/auth/session`. [#1219](https://github.com/sourcebot-dev/sourcebot/pull/1219)
1515
## [4.17.2] - 2026-05-16
1616

1717
### Added

packages/web/src/auth.ts

Lines changed: 58 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,15 @@ const nextAuthResult = NextAuth({
214214
signOut: async (message) => {
215215
const token = message as { token: { userId: string } | null };
216216
if (token?.token?.userId) {
217+
// Bump sessionVersion so any JWT minted before this signout
218+
// is treated as invalid by the jwt callback's DB cross-check
219+
// on its next request, even if the cookie value was captured
220+
// and is being replayed.
221+
await __unsafePrisma.user.update({
222+
where: { id: token.token.userId },
223+
data: { sessionVersion: { increment: 1 } },
224+
});
225+
217226
await auditService.createAudit({
218227
action: "user.signed_out",
219228
actor: {
@@ -259,20 +268,51 @@ const nextAuthResult = NextAuth({
259268
token.sessionVersion = user.sessionVersion ?? 0;
260269
}
261270

262-
// @note The following performs a lazy migration of the issuerUrl
263-
// in the user's accounts. The issuerUrl was introduced in v4.15.4
264-
// and will not be present for accounts created prior to this version.
265-
//
266-
// @see https://github.com/sourcebot-dev/sourcebot/pull/993
267271
if (token.userId) {
268-
const accountsWithoutIssuerUrl = await __unsafePrisma.account.findMany({
272+
// Single query: fetch the user's current sessionVersion for
273+
// the cross-check below, plus any accounts that still need
274+
// the issuerUrl lazy migration.
275+
//
276+
// @see https://github.com/sourcebot-dev/sourcebot/pull/993
277+
const dbUser = await __unsafePrisma.user.findUnique({
269278
where: {
270-
userId: token.userId,
271-
issuerUrl: null,
279+
id: token.userId as string,
280+
},
281+
select: {
282+
sessionVersion: true,
283+
accounts: {
284+
where: {
285+
issuerUrl: null,
286+
},
287+
},
272288
},
273289
});
274290

275-
for (const account of accountsWithoutIssuerUrl) {
291+
// The user row was removed (e.g., deleted via /api/ee/user
292+
// or org-removal cascade). Treat the JWT as invalid so
293+
// /api/auth/session reports logged-out and @auth/core clears
294+
// the cookie from the browser.
295+
if (!dbUser) {
296+
return null;
297+
}
298+
299+
// On every non-login request, cross-check the JWT's
300+
// sessionVersion against the user's current sessionVersion in
301+
// the database. A mismatch means the user signed out, was
302+
// removed from the org, or their sessions were otherwise
303+
// invalidated since the JWT was minted. Returning null here
304+
// is what makes invalidation visible at /api/auth/session,
305+
// not just at withAuth-gated endpoints.
306+
const tokenSessionVersion = token.sessionVersion ?? 0;
307+
if (!user && tokenSessionVersion !== dbUser.sessionVersion) {
308+
return null;
309+
}
310+
311+
// Lazy migration of issuerUrl on accounts created before
312+
// the column was introduced in v4.15.4. The where clause
313+
// above scopes this to only accounts that still need it,
314+
// so the loop is a no-op once everyone is backfilled.
315+
for (const account of dbUser.accounts) {
276316
const issuerUrl = await getIssuerUrlForAccount(account);
277317
if (issuerUrl) {
278318
await __unsafePrisma.account.update({
@@ -313,35 +353,18 @@ const nextAuthResult = NextAuth({
313353
export const { handlers, signIn, signOut } = nextAuthResult;
314354

315355
/**
316-
* Wrapped session resolver that enforces JWT versioning at the auth layer.
356+
* Per-request memoized session resolver.
317357
*
318-
* Every JWT cookie carries the `sessionVersion` it was minted with. This
319-
* wrapper compares it against the user's current `sessionVersion` in the
320-
* database; if the user's version has been bumped (e.g., they were removed
321-
* from the org), we return null so every caller of `auth()` sees the
322-
* session as logged out.
358+
* JWT validity (including the `sessionVersion` cross-check against the
359+
* database and the existence of the underlying `User` row) is enforced in
360+
* the `jwt` callback above. If that callback returns `null`, NextAuth's
361+
* core resolves the session to `null` here and also clears the cookie on
362+
* the response. We therefore only need to memoize the result within a
363+
* single request so that multiple `auth()` callers share the same answer
364+
* without re-running the upstream resolver.
323365
*/
324366
export const auth = cache(async (): Promise<Session | null> => {
325-
const session = await nextAuthResult.auth();
326-
if (!session) {
327-
return null;
328-
}
329-
330-
const dbUser = await __unsafePrisma.user.findUnique({
331-
where: { id: session.user.id },
332-
select: { sessionVersion: true },
333-
});
334-
335-
if (!dbUser) {
336-
return null;
337-
}
338-
339-
const tokenVersion = session.sessionVersion ?? 0;
340-
if (tokenVersion !== dbUser.sessionVersion) {
341-
return null;
342-
}
343-
344-
return session;
367+
return nextAuthResult.auth();
345368
});
346369

347370
/**

0 commit comments

Comments
 (0)