feat(web): JWT session versioning and credential revocation on org removal - #1168

Merged
brendan-kellam merged 4 commits into
mainfrom
brendan/jwt-versioning
May 4, 2026
Merged

feat(web): JWT session versioning and credential revocation on org removal#1168
brendan-kellam merged 4 commits into
mainfrom
brendan/jwt-versioning

Conversation

@brendan-kellam

@brendan-kellambrendan-kellam commented May 2, 2026

Copy link
Copy Markdown
Contributor

This PR adds a sessionVersion integer to the User table and JWT token. Whenever auth is called, we compare the version stored in the database and the token. If they don't match, then the session is invalid (null). This allows us to invalidate JWT tokens from the server by incrementing the sessionVersion for a given user.

We use this capability in the user removal path to sign out users when the admin removes them from the organization.

Test plan

  • Sign in as a test user, hit /api/repos, confirm 200.
  • Have an admin remove the test user via Settings → Members.
  • On the test user's next request (page or API): auth() returns null, withAuth rejects, page redirects to /login.
  • Issue an API key as the test user, then have an admin remove them. Confirm the API key returns 401 on the next call (the ApiKey row is gone).
  • If the deployment uses MCP/OAuth (e.g., Claude Desktop), revoke a user with an active OAuth token and confirm subsequent MCP calls 401.
  • Re-add the test user via invite → they sign in cleanly with a fresh JWT carrying the bumped sessionVersion.
  • Confirm pre-existing sessions for other users are unaffected (no incidental version bumps).
  • leaveOrg — same cascade behavior when a non-owner leaves voluntarily.
  • leaveOrg — last-owner guard still rejects the action.
  • Existing JWT cookies issued before this PR shipped continue to work (backwards compat — they have no sessionVersion claim and fall back to 0).

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Automatic session invalidation: when a user is removed or leaves an organization, their active sessions, OAuth tokens, and API keys are atomically revoked and take effect on their next request.
    • Audit actions: added organization membership lifecycle audit events for member added, removed, and left.

…moval
Adds a per-user `sessionVersion` integer to the `User` model. The version is
baked into every newly-minted JWT cookie via the `jwt` callback, copied onto
the session via the `session` callback, and verified on every read by a
wrapped `auth()` function that compares the cookie's claim against the
current DB value — mismatch returns null, treating the session as logged out
on the very next request.
Backwards compatible: pre-migration cookies have no claim and fall back to 0,
which matches the default User.sessionVersion of 0, so existing sessions
keep working until something explicitly bumps the user's version.
The `auth()` wrapper is memoized per-request via React `cache()` so the
extra DB read happens at most once per request even though `auth()` is
called from many places (layout, page, withAuth, getAuthenticatedUser).
`removeMemberFromOrg` and `leaveOrg` now run three credential-revocation
helpers inside the existing serializable transaction:
- `invalidateAllSessionsForUser` — bumps the version, killing every active
JWT cookie for the user on their next request.
- `revokeUserOAuthTokens` — deletes their `OAuthToken`,
`OAuthRefreshToken`, and `OAuthAuthorizationCode` rows. Not org-scoped
because OAuthClient has no `orgId`.
- `revokeUserApiKeysInOrg` — deletes their `ApiKey` rows scoped to the
current org (ApiKey.orgId).
Net effect: when an admin removes a member (or a member leaves), the user's
JWT cookie, personal API keys for that org, and OAuth tokens all stop
working atomically. A failed transaction rolls back all four changes.
@github-actions

This comment has been minimized.

@coderabbitai

coderabbitaiBot commented May 2, 2026

Copy link
Copy Markdown
Contributor

Walkthrough

Adds per-user sessionVersion (DB + Prisma) used in JWTs; NextAuth propagates and validates the claim via a cached auth() that returns null on mismatch. removeMemberFromOrg/leaveOrg now increment sessionVersion and revoke a user’s OAuth tokens and org API keys inside the same Prisma transaction. CHANGELOG and mocks updated.

Changes

Per-User JWT Session Versioning & Membership Revocation

Layer / File(s)Summary
Data Shape / Migration
packages/db/prisma/migrations/20260501170139_add_user_session_version/migration.sql, packages/db/prisma/schema.prisma
Adds sessionVersion Int @default(0) to User and creates a DB column with default 0.
Type Augmentation
packages/web/src/auth.ts
Augments next-auth and next-auth/jwt types: Session, User, and JWT include sessionVersion?: number.
Auth Core: JWT & Session Propagation
packages/web/src/auth.ts
Credentials provider returns sessionVersion; jwt callback writes token.sessionVersion; session callback copies token.sessionVersion into session.
Auth Wiring: cached auth() Validation
packages/web/src/auth.ts
NextAuth result is stored in nextAuthResult; exports handlers, signIn, signOut; new auth = cache(async () => ...) calls nextAuthResult.auth(), loads user from DB and returns null if DB sessionVersion ≠ JWT sessionVersion.
Membership Flows & Revocation (transactional)
packages/web/src/features/userManagement/actions.ts
removeMemberFromOrg and leaveOrg now, inside the same Serializable Prisma transaction, call helpers that increment the user’s sessionVersion, delete OAuth tokens/authorization codes/refresh tokens, and delete org API keys before deleting the membership record.
Helpers / Revocation Implementation
packages/web/src/features/userManagement/actions.ts
Adds invalidateAllSessionsForUser(prisma, userId), revokeUserOAuthTokens(prisma, userId), and revokeUserApiKeysInOrg(prisma, userId, orgId) that perform the DB deletes/updates via the transaction client.
Mocks & Changelog
packages/web/src/__mocks__/prisma.ts, CHANGELOG.md
Mock user gets sessionVersion: 0; CHANGELOG documents org membership audit-actions and session versioning behavior.

Sequence Diagram

sequenceDiagram
participant Client
participant NextAuth as NextAuth<br/>(middleware)
participant Auth as auth()<br/>(cache)
participant DB as Database
participant API as Protected<br/>Route
Client->>NextAuth: Request with JWT cookie (sessionVersion)
NextAuth->>Auth: call auth()
Auth->>DB: fetch user by sub
DB-->>Auth: user (sessionVersion = X)
alt X == JWT.sessionVersion
Auth-->>NextAuth: session object
NextAuth->>API: forward request (authorized)
API-->>Client: 200 OK
else mismatch
Auth-->>NextAuth: null
NextAuth-->>Client: 401 Unauthorized
end
Loading

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly Related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check nameStatusExplanation
Title check✅ PassedThe title 'feat(web): JWT session versioning and credential revocation on org removal' accurately describes the main changes: JWT session versioning and credential revocation on user removal/org departure.
Docstring Coverage✅ PassedNo functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch brendan/jwt-versioning

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@CHANGELOG.md`:
- Around line 10-11: The changelog entry currently under "### Added" describing
per-user JWT session versioning is a fix, not a new feature; move the entire
bullet ("Added per-user JWT session versioning so admin-driven member
removals... [`#1168`](https://github.com/sourcebot-dev/sourcebot/pull/1168)") out
of the "### Added" section and append it to the bottom of the "### Fixed"
section, preserving the exact text and PR link and leaving other entries/order
unchanged.
In `@packages/web/src/features/userManagement/actions.ts`:
- Around line 130-161: The new revocation functions revokeUserApiKeysInOrg and
revokeUserOAuthTokens perform unindexed deleteMany queries; add appropriate
indexes to avoid full-table scans by updating the Prisma schema: add a composite
index on ApiKey for (createdById, orgId) and add single-column indexes on
OAuthToken.userId, OAuthRefreshToken.userId, and OAuthAuthorizationCode.userId
(or composite if you prefer specific access patterns), then generate and apply a
migration so the deleteMany calls run against indexed columns within the
transaction.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 21dca91d-f46d-4355-a889-59c628f6d5d5

📥 Commits

Reviewing files that changed from the base of the PR and between ff41d83 and 5b9b3ed.

📒 Files selected for processing (6)
  • CHANGELOG.md
  • packages/db/prisma/migrations/20260501170139_add_user_session_version/migration.sql
  • packages/db/prisma/schema.prisma
  • packages/web/src/__mocks__/prisma.ts
  • packages/web/src/auth.ts
  • packages/web/src/features/userManagement/actions.ts

Comment threadCHANGELOG.md
Comment threadpackages/web/src/features/userManagement/actions.ts
@brendan-kellam
brendan-kellam merged commit 7243fdf into mainMay 4, 2026
8 checks passed
@brendan-kellam
brendan-kellam deleted the brendan/jwt-versioning branch May 4, 2026 18:17
@github-actionsgithub-actionsBot mentioned this pull request May 4, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@brendan-kellam
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all \u003cpre\u003e\u003ccode\u003e blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks"); } } catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); } })(); (function(){ try { var __m = "github.com"; var __re = new RegExp('^' + "github\\.com" + '
Skip to content

feat(web): JWT session versioning and credential revocation on org removal - #1168

Merged
brendan-kellam merged 4 commits into
mainfrom
brendan/jwt-versioning
May 4, 2026
Merged

feat(web): JWT session versioning and credential revocation on org removal#1168
brendan-kellam merged 4 commits into
mainfrom
brendan/jwt-versioning

Conversation

@brendan-kellam

@brendan-kellambrendan-kellam commented May 2, 2026

Copy link
Copy Markdown
Contributor

This PR adds a sessionVersion integer to the User table and JWT token. Whenever auth is called, we compare the version stored in the database and the token. If they don't match, then the session is invalid (null). This allows us to invalidate JWT tokens from the server by incrementing the sessionVersion for a given user.

We use this capability in the user removal path to sign out users when the admin removes them from the organization.

Test plan

  • Sign in as a test user, hit /api/repos, confirm 200.
  • Have an admin remove the test user via Settings → Members.
  • On the test user's next request (page or API): auth() returns null, withAuth rejects, page redirects to /login.
  • Issue an API key as the test user, then have an admin remove them. Confirm the API key returns 401 on the next call (the ApiKey row is gone).
  • If the deployment uses MCP/OAuth (e.g., Claude Desktop), revoke a user with an active OAuth token and confirm subsequent MCP calls 401.
  • Re-add the test user via invite → they sign in cleanly with a fresh JWT carrying the bumped sessionVersion.
  • Confirm pre-existing sessions for other users are unaffected (no incidental version bumps).
  • leaveOrg — same cascade behavior when a non-owner leaves voluntarily.
  • leaveOrg — last-owner guard still rejects the action.
  • Existing JWT cookies issued before this PR shipped continue to work (backwards compat — they have no sessionVersion claim and fall back to 0).

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Automatic session invalidation: when a user is removed or leaves an organization, their active sessions, OAuth tokens, and API keys are atomically revoked and take effect on their next request.
    • Audit actions: added organization membership lifecycle audit events for member added, removed, and left.

…moval
Adds a per-user `sessionVersion` integer to the `User` model. The version is
baked into every newly-minted JWT cookie via the `jwt` callback, copied onto
the session via the `session` callback, and verified on every read by a
wrapped `auth()` function that compares the cookie's claim against the
current DB value — mismatch returns null, treating the session as logged out
on the very next request.
Backwards compatible: pre-migration cookies have no claim and fall back to 0,
which matches the default User.sessionVersion of 0, so existing sessions
keep working until something explicitly bumps the user's version.
The `auth()` wrapper is memoized per-request via React `cache()` so the
extra DB read happens at most once per request even though `auth()` is
called from many places (layout, page, withAuth, getAuthenticatedUser).
`removeMemberFromOrg` and `leaveOrg` now run three credential-revocation
helpers inside the existing serializable transaction:
- `invalidateAllSessionsForUser` — bumps the version, killing every active
JWT cookie for the user on their next request.
- `revokeUserOAuthTokens` — deletes their `OAuthToken`,
`OAuthRefreshToken`, and `OAuthAuthorizationCode` rows. Not org-scoped
because OAuthClient has no `orgId`.
- `revokeUserApiKeysInOrg` — deletes their `ApiKey` rows scoped to the
current org (ApiKey.orgId).
Net effect: when an admin removes a member (or a member leaves), the user's
JWT cookie, personal API keys for that org, and OAuth tokens all stop
working atomically. A failed transaction rolls back all four changes.
@github-actions

This comment has been minimized.

@coderabbitai

coderabbitaiBot commented May 2, 2026

Copy link
Copy Markdown
Contributor

Walkthrough

Adds per-user sessionVersion (DB + Prisma) used in JWTs; NextAuth propagates and validates the claim via a cached auth() that returns null on mismatch. removeMemberFromOrg/leaveOrg now increment sessionVersion and revoke a user’s OAuth tokens and org API keys inside the same Prisma transaction. CHANGELOG and mocks updated.

Changes

Per-User JWT Session Versioning & Membership Revocation

Layer / File(s)Summary
Data Shape / Migration
packages/db/prisma/migrations/20260501170139_add_user_session_version/migration.sql, packages/db/prisma/schema.prisma
Adds sessionVersion Int @default(0) to User and creates a DB column with default 0.
Type Augmentation
packages/web/src/auth.ts
Augments next-auth and next-auth/jwt types: Session, User, and JWT include sessionVersion?: number.
Auth Core: JWT & Session Propagation
packages/web/src/auth.ts
Credentials provider returns sessionVersion; jwt callback writes token.sessionVersion; session callback copies token.sessionVersion into session.
Auth Wiring: cached auth() Validation
packages/web/src/auth.ts
NextAuth result is stored in nextAuthResult; exports handlers, signIn, signOut; new auth = cache(async () => ...) calls nextAuthResult.auth(), loads user from DB and returns null if DB sessionVersion ≠ JWT sessionVersion.
Membership Flows & Revocation (transactional)
packages/web/src/features/userManagement/actions.ts
removeMemberFromOrg and leaveOrg now, inside the same Serializable Prisma transaction, call helpers that increment the user’s sessionVersion, delete OAuth tokens/authorization codes/refresh tokens, and delete org API keys before deleting the membership record.
Helpers / Revocation Implementation
packages/web/src/features/userManagement/actions.ts
Adds invalidateAllSessionsForUser(prisma, userId), revokeUserOAuthTokens(prisma, userId), and revokeUserApiKeysInOrg(prisma, userId, orgId) that perform the DB deletes/updates via the transaction client.
Mocks & Changelog
packages/web/src/__mocks__/prisma.ts, CHANGELOG.md
Mock user gets sessionVersion: 0; CHANGELOG documents org membership audit-actions and session versioning behavior.

Sequence Diagram

sequenceDiagram
participant Client
participant NextAuth as NextAuth<br/>(middleware)
participant Auth as auth()<br/>(cache)
participant DB as Database
participant API as Protected<br/>Route
Client->>NextAuth: Request with JWT cookie (sessionVersion)
NextAuth->>Auth: call auth()
Auth->>DB: fetch user by sub
DB-->>Auth: user (sessionVersion = X)
alt X == JWT.sessionVersion
Auth-->>NextAuth: session object
NextAuth->>API: forward request (authorized)
API-->>Client: 200 OK
else mismatch
Auth-->>NextAuth: null
NextAuth-->>Client: 401 Unauthorized
end
Loading

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly Related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check nameStatusExplanation
Title check✅ PassedThe title 'feat(web): JWT session versioning and credential revocation on org removal' accurately describes the main changes: JWT session versioning and credential revocation on user removal/org departure.
Docstring Coverage✅ PassedNo functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch brendan/jwt-versioning

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@CHANGELOG.md`:
- Around line 10-11: The changelog entry currently under "### Added" describing
per-user JWT session versioning is a fix, not a new feature; move the entire
bullet ("Added per-user JWT session versioning so admin-driven member
removals... [`#1168`](https://github.com/sourcebot-dev/sourcebot/pull/1168)") out
of the "### Added" section and append it to the bottom of the "### Fixed"
section, preserving the exact text and PR link and leaving other entries/order
unchanged.
In `@packages/web/src/features/userManagement/actions.ts`:
- Around line 130-161: The new revocation functions revokeUserApiKeysInOrg and
revokeUserOAuthTokens perform unindexed deleteMany queries; add appropriate
indexes to avoid full-table scans by updating the Prisma schema: add a composite
index on ApiKey for (createdById, orgId) and add single-column indexes on
OAuthToken.userId, OAuthRefreshToken.userId, and OAuthAuthorizationCode.userId
(or composite if you prefer specific access patterns), then generate and apply a
migration so the deleteMany calls run against indexed columns within the
transaction.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 21dca91d-f46d-4355-a889-59c628f6d5d5

📥 Commits

Reviewing files that changed from the base of the PR and between ff41d83 and 5b9b3ed.

📒 Files selected for processing (6)
  • CHANGELOG.md
  • packages/db/prisma/migrations/20260501170139_add_user_session_version/migration.sql
  • packages/db/prisma/schema.prisma
  • packages/web/src/__mocks__/prisma.ts
  • packages/web/src/auth.ts
  • packages/web/src/features/userManagement/actions.ts

Comment threadCHANGELOG.md
Comment threadpackages/web/src/features/userManagement/actions.ts
@brendan-kellam
brendan-kellam merged commit 7243fdf into mainMay 4, 2026
8 checks passed
@brendan-kellam
brendan-kellam deleted the brendan/jwt-versioning branch May 4, 2026 18:17
@github-actionsgithub-actionsBot mentioned this pull request May 4, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@brendan-kellam
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(web): JWT session versioning and credential revocation on org removal - #1168

Merged
brendan-kellam merged 4 commits into
mainfrom
brendan/jwt-versioning
May 4, 2026
Merged

feat(web): JWT session versioning and credential revocation on org removal#1168
brendan-kellam merged 4 commits into
mainfrom
brendan/jwt-versioning

Conversation

@brendan-kellam

@brendan-kellambrendan-kellam commented May 2, 2026

Copy link
Copy Markdown
Contributor

This PR adds a sessionVersion integer to the User table and JWT token. Whenever auth is called, we compare the version stored in the database and the token. If they don't match, then the session is invalid (null). This allows us to invalidate JWT tokens from the server by incrementing the sessionVersion for a given user.

We use this capability in the user removal path to sign out users when the admin removes them from the organization.

Test plan

  • Sign in as a test user, hit /api/repos, confirm 200.
  • Have an admin remove the test user via Settings → Members.
  • On the test user's next request (page or API): auth() returns null, withAuth rejects, page redirects to /login.
  • Issue an API key as the test user, then have an admin remove them. Confirm the API key returns 401 on the next call (the ApiKey row is gone).
  • If the deployment uses MCP/OAuth (e.g., Claude Desktop), revoke a user with an active OAuth token and confirm subsequent MCP calls 401.
  • Re-add the test user via invite → they sign in cleanly with a fresh JWT carrying the bumped sessionVersion.
  • Confirm pre-existing sessions for other users are unaffected (no incidental version bumps).
  • leaveOrg — same cascade behavior when a non-owner leaves voluntarily.
  • leaveOrg — last-owner guard still rejects the action.
  • Existing JWT cookies issued before this PR shipped continue to work (backwards compat — they have no sessionVersion claim and fall back to 0).

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Automatic session invalidation: when a user is removed or leaves an organization, their active sessions, OAuth tokens, and API keys are atomically revoked and take effect on their next request.
    • Audit actions: added organization membership lifecycle audit events for member added, removed, and left.

…moval
Adds a per-user `sessionVersion` integer to the `User` model. The version is
baked into every newly-minted JWT cookie via the `jwt` callback, copied onto
the session via the `session` callback, and verified on every read by a
wrapped `auth()` function that compares the cookie's claim against the
current DB value — mismatch returns null, treating the session as logged out
on the very next request.
Backwards compatible: pre-migration cookies have no claim and fall back to 0,
which matches the default User.sessionVersion of 0, so existing sessions
keep working until something explicitly bumps the user's version.
The `auth()` wrapper is memoized per-request via React `cache()` so the
extra DB read happens at most once per request even though `auth()` is
called from many places (layout, page, withAuth, getAuthenticatedUser).
`removeMemberFromOrg` and `leaveOrg` now run three credential-revocation
helpers inside the existing serializable transaction:
- `invalidateAllSessionsForUser` — bumps the version, killing every active
JWT cookie for the user on their next request.
- `revokeUserOAuthTokens` — deletes their `OAuthToken`,
`OAuthRefreshToken`, and `OAuthAuthorizationCode` rows. Not org-scoped
because OAuthClient has no `orgId`.
- `revokeUserApiKeysInOrg` — deletes their `ApiKey` rows scoped to the
current org (ApiKey.orgId).
Net effect: when an admin removes a member (or a member leaves), the user's
JWT cookie, personal API keys for that org, and OAuth tokens all stop
working atomically. A failed transaction rolls back all four changes.
@github-actions

This comment has been minimized.

@coderabbitai

coderabbitaiBot commented May 2, 2026

Copy link
Copy Markdown
Contributor

Walkthrough

Adds per-user sessionVersion (DB + Prisma) used in JWTs; NextAuth propagates and validates the claim via a cached auth() that returns null on mismatch. removeMemberFromOrg/leaveOrg now increment sessionVersion and revoke a user’s OAuth tokens and org API keys inside the same Prisma transaction. CHANGELOG and mocks updated.

Changes

Per-User JWT Session Versioning & Membership Revocation

Layer / File(s)Summary
Data Shape / Migration
packages/db/prisma/migrations/20260501170139_add_user_session_version/migration.sql, packages/db/prisma/schema.prisma
Adds sessionVersion Int @default(0) to User and creates a DB column with default 0.
Type Augmentation
packages/web/src/auth.ts
Augments next-auth and next-auth/jwt types: Session, User, and JWT include sessionVersion?: number.
Auth Core: JWT & Session Propagation
packages/web/src/auth.ts
Credentials provider returns sessionVersion; jwt callback writes token.sessionVersion; session callback copies token.sessionVersion into session.
Auth Wiring: cached auth() Validation
packages/web/src/auth.ts
NextAuth result is stored in nextAuthResult; exports handlers, signIn, signOut; new auth = cache(async () => ...) calls nextAuthResult.auth(), loads user from DB and returns null if DB sessionVersion ≠ JWT sessionVersion.
Membership Flows & Revocation (transactional)
packages/web/src/features/userManagement/actions.ts
removeMemberFromOrg and leaveOrg now, inside the same Serializable Prisma transaction, call helpers that increment the user’s sessionVersion, delete OAuth tokens/authorization codes/refresh tokens, and delete org API keys before deleting the membership record.
Helpers / Revocation Implementation
packages/web/src/features/userManagement/actions.ts
Adds invalidateAllSessionsForUser(prisma, userId), revokeUserOAuthTokens(prisma, userId), and revokeUserApiKeysInOrg(prisma, userId, orgId) that perform the DB deletes/updates via the transaction client.
Mocks & Changelog
packages/web/src/__mocks__/prisma.ts, CHANGELOG.md
Mock user gets sessionVersion: 0; CHANGELOG documents org membership audit-actions and session versioning behavior.

Sequence Diagram

sequenceDiagram
participant Client
participant NextAuth as NextAuth<br/>(middleware)
participant Auth as auth()<br/>(cache)
participant DB as Database
participant API as Protected<br/>Route
Client->>NextAuth: Request with JWT cookie (sessionVersion)
NextAuth->>Auth: call auth()
Auth->>DB: fetch user by sub
DB-->>Auth: user (sessionVersion = X)
alt X == JWT.sessionVersion
Auth-->>NextAuth: session object
NextAuth->>API: forward request (authorized)
API-->>Client: 200 OK
else mismatch
Auth-->>NextAuth: null
NextAuth-->>Client: 401 Unauthorized
end
Loading

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly Related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check nameStatusExplanation
Title check✅ PassedThe title 'feat(web): JWT session versioning and credential revocation on org removal' accurately describes the main changes: JWT session versioning and credential revocation on user removal/org departure.
Docstring Coverage✅ PassedNo functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch brendan/jwt-versioning

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@CHANGELOG.md`:
- Around line 10-11: The changelog entry currently under "### Added" describing
per-user JWT session versioning is a fix, not a new feature; move the entire
bullet ("Added per-user JWT session versioning so admin-driven member
removals... [`#1168`](https://github.com/sourcebot-dev/sourcebot/pull/1168)") out
of the "### Added" section and append it to the bottom of the "### Fixed"
section, preserving the exact text and PR link and leaving other entries/order
unchanged.
In `@packages/web/src/features/userManagement/actions.ts`:
- Around line 130-161: The new revocation functions revokeUserApiKeysInOrg and
revokeUserOAuthTokens perform unindexed deleteMany queries; add appropriate
indexes to avoid full-table scans by updating the Prisma schema: add a composite
index on ApiKey for (createdById, orgId) and add single-column indexes on
OAuthToken.userId, OAuthRefreshToken.userId, and OAuthAuthorizationCode.userId
(or composite if you prefer specific access patterns), then generate and apply a
migration so the deleteMany calls run against indexed columns within the
transaction.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 21dca91d-f46d-4355-a889-59c628f6d5d5

📥 Commits

Reviewing files that changed from the base of the PR and between ff41d83 and 5b9b3ed.

📒 Files selected for processing (6)
  • CHANGELOG.md
  • packages/db/prisma/migrations/20260501170139_add_user_session_version/migration.sql
  • packages/db/prisma/schema.prisma
  • packages/web/src/__mocks__/prisma.ts
  • packages/web/src/auth.ts
  • packages/web/src/features/userManagement/actions.ts

Comment threadCHANGELOG.md
Comment threadpackages/web/src/features/userManagement/actions.ts
@brendan-kellam
brendan-kellam merged commit 7243fdf into mainMay 4, 2026
8 checks passed
@brendan-kellam
brendan-kellam deleted the brendan/jwt-versioning branch May 4, 2026 18:17
@github-actionsgithub-actionsBot mentioned this pull request May 4, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@brendan-kellam
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length \u003e 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(web): JWT session versioning and credential revocation on org removal - #1168

Merged
brendan-kellam merged 4 commits into
mainfrom
brendan/jwt-versioning
May 4, 2026
Merged

feat(web): JWT session versioning and credential revocation on org removal#1168
brendan-kellam merged 4 commits into
mainfrom
brendan/jwt-versioning

Conversation

@brendan-kellam

@brendan-kellambrendan-kellam commented May 2, 2026

Copy link
Copy Markdown
Contributor

This PR adds a sessionVersion integer to the User table and JWT token. Whenever auth is called, we compare the version stored in the database and the token. If they don't match, then the session is invalid (null). This allows us to invalidate JWT tokens from the server by incrementing the sessionVersion for a given user.

We use this capability in the user removal path to sign out users when the admin removes them from the organization.

Test plan

  • Sign in as a test user, hit /api/repos, confirm 200.
  • Have an admin remove the test user via Settings → Members.
  • On the test user's next request (page or API): auth() returns null, withAuth rejects, page redirects to /login.
  • Issue an API key as the test user, then have an admin remove them. Confirm the API key returns 401 on the next call (the ApiKey row is gone).
  • If the deployment uses MCP/OAuth (e.g., Claude Desktop), revoke a user with an active OAuth token and confirm subsequent MCP calls 401.
  • Re-add the test user via invite → they sign in cleanly with a fresh JWT carrying the bumped sessionVersion.
  • Confirm pre-existing sessions for other users are unaffected (no incidental version bumps).
  • leaveOrg — same cascade behavior when a non-owner leaves voluntarily.
  • leaveOrg — last-owner guard still rejects the action.
  • Existing JWT cookies issued before this PR shipped continue to work (backwards compat — they have no sessionVersion claim and fall back to 0).

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Automatic session invalidation: when a user is removed or leaves an organization, their active sessions, OAuth tokens, and API keys are atomically revoked and take effect on their next request.
    • Audit actions: added organization membership lifecycle audit events for member added, removed, and left.

…moval
Adds a per-user `sessionVersion` integer to the `User` model. The version is
baked into every newly-minted JWT cookie via the `jwt` callback, copied onto
the session via the `session` callback, and verified on every read by a
wrapped `auth()` function that compares the cookie's claim against the
current DB value — mismatch returns null, treating the session as logged out
on the very next request.
Backwards compatible: pre-migration cookies have no claim and fall back to 0,
which matches the default User.sessionVersion of 0, so existing sessions
keep working until something explicitly bumps the user's version.
The `auth()` wrapper is memoized per-request via React `cache()` so the
extra DB read happens at most once per request even though `auth()` is
called from many places (layout, page, withAuth, getAuthenticatedUser).
`removeMemberFromOrg` and `leaveOrg` now run three credential-revocation
helpers inside the existing serializable transaction:
- `invalidateAllSessionsForUser` — bumps the version, killing every active
JWT cookie for the user on their next request.
- `revokeUserOAuthTokens` — deletes their `OAuthToken`,
`OAuthRefreshToken`, and `OAuthAuthorizationCode` rows. Not org-scoped
because OAuthClient has no `orgId`.
- `revokeUserApiKeysInOrg` — deletes their `ApiKey` rows scoped to the
current org (ApiKey.orgId).
Net effect: when an admin removes a member (or a member leaves), the user's
JWT cookie, personal API keys for that org, and OAuth tokens all stop
working atomically. A failed transaction rolls back all four changes.
@github-actions

This comment has been minimized.

@coderabbitai

coderabbitaiBot commented May 2, 2026

Copy link
Copy Markdown
Contributor

Walkthrough

Adds per-user sessionVersion (DB + Prisma) used in JWTs; NextAuth propagates and validates the claim via a cached auth() that returns null on mismatch. removeMemberFromOrg/leaveOrg now increment sessionVersion and revoke a user’s OAuth tokens and org API keys inside the same Prisma transaction. CHANGELOG and mocks updated.

Changes

Per-User JWT Session Versioning & Membership Revocation

Layer / File(s)Summary
Data Shape / Migration
packages/db/prisma/migrations/20260501170139_add_user_session_version/migration.sql, packages/db/prisma/schema.prisma
Adds sessionVersion Int @default(0) to User and creates a DB column with default 0.
Type Augmentation
packages/web/src/auth.ts
Augments next-auth and next-auth/jwt types: Session, User, and JWT include sessionVersion?: number.
Auth Core: JWT & Session Propagation
packages/web/src/auth.ts
Credentials provider returns sessionVersion; jwt callback writes token.sessionVersion; session callback copies token.sessionVersion into session.
Auth Wiring: cached auth() Validation
packages/web/src/auth.ts
NextAuth result is stored in nextAuthResult; exports handlers, signIn, signOut; new auth = cache(async () => ...) calls nextAuthResult.auth(), loads user from DB and returns null if DB sessionVersion ≠ JWT sessionVersion.
Membership Flows & Revocation (transactional)
packages/web/src/features/userManagement/actions.ts
removeMemberFromOrg and leaveOrg now, inside the same Serializable Prisma transaction, call helpers that increment the user’s sessionVersion, delete OAuth tokens/authorization codes/refresh tokens, and delete org API keys before deleting the membership record.
Helpers / Revocation Implementation
packages/web/src/features/userManagement/actions.ts
Adds invalidateAllSessionsForUser(prisma, userId), revokeUserOAuthTokens(prisma, userId), and revokeUserApiKeysInOrg(prisma, userId, orgId) that perform the DB deletes/updates via the transaction client.
Mocks & Changelog
packages/web/src/__mocks__/prisma.ts, CHANGELOG.md
Mock user gets sessionVersion: 0; CHANGELOG documents org membership audit-actions and session versioning behavior.

Sequence Diagram

sequenceDiagram
participant Client
participant NextAuth as NextAuth<br/>(middleware)
participant Auth as auth()<br/>(cache)
participant DB as Database
participant API as Protected<br/>Route
Client->>NextAuth: Request with JWT cookie (sessionVersion)
NextAuth->>Auth: call auth()
Auth->>DB: fetch user by sub
DB-->>Auth: user (sessionVersion = X)
alt X == JWT.sessionVersion
Auth-->>NextAuth: session object
NextAuth->>API: forward request (authorized)
API-->>Client: 200 OK
else mismatch
Auth-->>NextAuth: null
NextAuth-->>Client: 401 Unauthorized
end
Loading

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly Related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check nameStatusExplanation
Title check✅ PassedThe title 'feat(web): JWT session versioning and credential revocation on org removal' accurately describes the main changes: JWT session versioning and credential revocation on user removal/org departure.
Docstring Coverage✅ PassedNo functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch brendan/jwt-versioning

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@CHANGELOG.md`:
- Around line 10-11: The changelog entry currently under "### Added" describing
per-user JWT session versioning is a fix, not a new feature; move the entire
bullet ("Added per-user JWT session versioning so admin-driven member
removals... [`#1168`](https://github.com/sourcebot-dev/sourcebot/pull/1168)") out
of the "### Added" section and append it to the bottom of the "### Fixed"
section, preserving the exact text and PR link and leaving other entries/order
unchanged.
In `@packages/web/src/features/userManagement/actions.ts`:
- Around line 130-161: The new revocation functions revokeUserApiKeysInOrg and
revokeUserOAuthTokens perform unindexed deleteMany queries; add appropriate
indexes to avoid full-table scans by updating the Prisma schema: add a composite
index on ApiKey for (createdById, orgId) and add single-column indexes on
OAuthToken.userId, OAuthRefreshToken.userId, and OAuthAuthorizationCode.userId
(or composite if you prefer specific access patterns), then generate and apply a
migration so the deleteMany calls run against indexed columns within the
transaction.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 21dca91d-f46d-4355-a889-59c628f6d5d5

📥 Commits

Reviewing files that changed from the base of the PR and between ff41d83 and 5b9b3ed.

📒 Files selected for processing (6)
  • CHANGELOG.md
  • packages/db/prisma/migrations/20260501170139_add_user_session_version/migration.sql
  • packages/db/prisma/schema.prisma
  • packages/web/src/__mocks__/prisma.ts
  • packages/web/src/auth.ts
  • packages/web/src/features/userManagement/actions.ts

Comment threadCHANGELOG.md
Comment threadpackages/web/src/features/userManagement/actions.ts
@brendan-kellam
brendan-kellam merged commit 7243fdf into mainMay 4, 2026
8 checks passed
@brendan-kellam
brendan-kellam deleted the brendan/jwt-versioning branch May 4, 2026 18:17
@github-actionsgithub-actionsBot mentioned this pull request May 4, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@brendan-kellam
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

feat(web): JWT session versioning and credential revocation on org removal - #1168

Merged
brendan-kellam merged 4 commits into
mainfrom
brendan/jwt-versioning
May 4, 2026
Merged

feat(web): JWT session versioning and credential revocation on org removal#1168
brendan-kellam merged 4 commits into
mainfrom
brendan/jwt-versioning

Conversation

@brendan-kellam

@brendan-kellambrendan-kellam commented May 2, 2026

Copy link
Copy Markdown
Contributor

This PR adds a sessionVersion integer to the User table and JWT token. Whenever auth is called, we compare the version stored in the database and the token. If they don't match, then the session is invalid (null). This allows us to invalidate JWT tokens from the server by incrementing the sessionVersion for a given user.

We use this capability in the user removal path to sign out users when the admin removes them from the organization.

Test plan

  • Sign in as a test user, hit /api/repos, confirm 200.
  • Have an admin remove the test user via Settings → Members.
  • On the test user's next request (page or API): auth() returns null, withAuth rejects, page redirects to /login.
  • Issue an API key as the test user, then have an admin remove them. Confirm the API key returns 401 on the next call (the ApiKey row is gone).
  • If the deployment uses MCP/OAuth (e.g., Claude Desktop), revoke a user with an active OAuth token and confirm subsequent MCP calls 401.
  • Re-add the test user via invite → they sign in cleanly with a fresh JWT carrying the bumped sessionVersion.
  • Confirm pre-existing sessions for other users are unaffected (no incidental version bumps).
  • leaveOrg — same cascade behavior when a non-owner leaves voluntarily.
  • leaveOrg — last-owner guard still rejects the action.
  • Existing JWT cookies issued before this PR shipped continue to work (backwards compat — they have no sessionVersion claim and fall back to 0).

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Automatic session invalidation: when a user is removed or leaves an organization, their active sessions, OAuth tokens, and API keys are atomically revoked and take effect on their next request.
    • Audit actions: added organization membership lifecycle audit events for member added, removed, and left.

…moval
Adds a per-user `sessionVersion` integer to the `User` model. The version is
baked into every newly-minted JWT cookie via the `jwt` callback, copied onto
the session via the `session` callback, and verified on every read by a
wrapped `auth()` function that compares the cookie's claim against the
current DB value — mismatch returns null, treating the session as logged out
on the very next request.
Backwards compatible: pre-migration cookies have no claim and fall back to 0,
which matches the default User.sessionVersion of 0, so existing sessions
keep working until something explicitly bumps the user's version.
The `auth()` wrapper is memoized per-request via React `cache()` so the
extra DB read happens at most once per request even though `auth()` is
called from many places (layout, page, withAuth, getAuthenticatedUser).
`removeMemberFromOrg` and `leaveOrg` now run three credential-revocation
helpers inside the existing serializable transaction:
- `invalidateAllSessionsForUser` — bumps the version, killing every active
JWT cookie for the user on their next request.
- `revokeUserOAuthTokens` — deletes their `OAuthToken`,
`OAuthRefreshToken`, and `OAuthAuthorizationCode` rows. Not org-scoped
because OAuthClient has no `orgId`.
- `revokeUserApiKeysInOrg` — deletes their `ApiKey` rows scoped to the
current org (ApiKey.orgId).
Net effect: when an admin removes a member (or a member leaves), the user's
JWT cookie, personal API keys for that org, and OAuth tokens all stop
working atomically. A failed transaction rolls back all four changes.
@github-actions

This comment has been minimized.

@coderabbitai

coderabbitaiBot commented May 2, 2026

Copy link
Copy Markdown
Contributor

Walkthrough

Adds per-user sessionVersion (DB + Prisma) used in JWTs; NextAuth propagates and validates the claim via a cached auth() that returns null on mismatch. removeMemberFromOrg/leaveOrg now increment sessionVersion and revoke a user’s OAuth tokens and org API keys inside the same Prisma transaction. CHANGELOG and mocks updated.

Changes

Per-User JWT Session Versioning & Membership Revocation

Layer / File(s)Summary
Data Shape / Migration
packages/db/prisma/migrations/20260501170139_add_user_session_version/migration.sql, packages/db/prisma/schema.prisma
Adds sessionVersion Int @default(0) to User and creates a DB column with default 0.
Type Augmentation
packages/web/src/auth.ts
Augments next-auth and next-auth/jwt types: Session, User, and JWT include sessionVersion?: number.
Auth Core: JWT & Session Propagation
packages/web/src/auth.ts
Credentials provider returns sessionVersion; jwt callback writes token.sessionVersion; session callback copies token.sessionVersion into session.
Auth Wiring: cached auth() Validation
packages/web/src/auth.ts
NextAuth result is stored in nextAuthResult; exports handlers, signIn, signOut; new auth = cache(async () => ...) calls nextAuthResult.auth(), loads user from DB and returns null if DB sessionVersion ≠ JWT sessionVersion.
Membership Flows & Revocation (transactional)
packages/web/src/features/userManagement/actions.ts
removeMemberFromOrg and leaveOrg now, inside the same Serializable Prisma transaction, call helpers that increment the user’s sessionVersion, delete OAuth tokens/authorization codes/refresh tokens, and delete org API keys before deleting the membership record.
Helpers / Revocation Implementation
packages/web/src/features/userManagement/actions.ts
Adds invalidateAllSessionsForUser(prisma, userId), revokeUserOAuthTokens(prisma, userId), and revokeUserApiKeysInOrg(prisma, userId, orgId) that perform the DB deletes/updates via the transaction client.
Mocks & Changelog
packages/web/src/__mocks__/prisma.ts, CHANGELOG.md
Mock user gets sessionVersion: 0; CHANGELOG documents org membership audit-actions and session versioning behavior.

Sequence Diagram

sequenceDiagram
participant Client
participant NextAuth as NextAuth<br/>(middleware)
participant Auth as auth()<br/>(cache)
participant DB as Database
participant API as Protected<br/>Route
Client->>NextAuth: Request with JWT cookie (sessionVersion)
NextAuth->>Auth: call auth()
Auth->>DB: fetch user by sub
DB-->>Auth: user (sessionVersion = X)
alt X == JWT.sessionVersion
Auth-->>NextAuth: session object
NextAuth->>API: forward request (authorized)
API-->>Client: 200 OK
else mismatch
Auth-->>NextAuth: null
NextAuth-->>Client: 401 Unauthorized
end
Loading

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly Related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check nameStatusExplanation
Title check✅ PassedThe title 'feat(web): JWT session versioning and credential revocation on org removal' accurately describes the main changes: JWT session versioning and credential revocation on user removal/org departure.
Docstring Coverage✅ PassedNo functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch brendan/jwt-versioning

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@CHANGELOG.md`:
- Around line 10-11: The changelog entry currently under "### Added" describing
per-user JWT session versioning is a fix, not a new feature; move the entire
bullet ("Added per-user JWT session versioning so admin-driven member
removals... [`#1168`](https://github.com/sourcebot-dev/sourcebot/pull/1168)") out
of the "### Added" section and append it to the bottom of the "### Fixed"
section, preserving the exact text and PR link and leaving other entries/order
unchanged.
In `@packages/web/src/features/userManagement/actions.ts`:
- Around line 130-161: The new revocation functions revokeUserApiKeysInOrg and
revokeUserOAuthTokens perform unindexed deleteMany queries; add appropriate
indexes to avoid full-table scans by updating the Prisma schema: add a composite
index on ApiKey for (createdById, orgId) and add single-column indexes on
OAuthToken.userId, OAuthRefreshToken.userId, and OAuthAuthorizationCode.userId
(or composite if you prefer specific access patterns), then generate and apply a
migration so the deleteMany calls run against indexed columns within the
transaction.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 21dca91d-f46d-4355-a889-59c628f6d5d5

📥 Commits

Reviewing files that changed from the base of the PR and between ff41d83 and 5b9b3ed.

📒 Files selected for processing (6)
  • CHANGELOG.md
  • packages/db/prisma/migrations/20260501170139_add_user_session_version/migration.sql
  • packages/db/prisma/schema.prisma
  • packages/web/src/__mocks__/prisma.ts
  • packages/web/src/auth.ts
  • packages/web/src/features/userManagement/actions.ts

Comment threadCHANGELOG.md
Comment threadpackages/web/src/features/userManagement/actions.ts
@brendan-kellam
brendan-kellam merged commit 7243fdf into mainMay 4, 2026
8 checks passed
@brendan-kellam
brendan-kellam deleted the brendan/jwt-versioning branch May 4, 2026 18:17
@github-actionsgithub-actionsBot mentioned this pull request May 4, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@brendan-kellam
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(web): JWT session versioning and credential revocation on org removal - #1168

Merged
brendan-kellam merged 4 commits into
mainfrom
brendan/jwt-versioning
May 4, 2026
Merged

feat(web): JWT session versioning and credential revocation on org removal#1168
brendan-kellam merged 4 commits into
mainfrom
brendan/jwt-versioning

Conversation

@brendan-kellam

@brendan-kellambrendan-kellam commented May 2, 2026

Copy link
Copy Markdown
Contributor

This PR adds a sessionVersion integer to the User table and JWT token. Whenever auth is called, we compare the version stored in the database and the token. If they don't match, then the session is invalid (null). This allows us to invalidate JWT tokens from the server by incrementing the sessionVersion for a given user.

We use this capability in the user removal path to sign out users when the admin removes them from the organization.

Test plan

  • Sign in as a test user, hit /api/repos, confirm 200.
  • Have an admin remove the test user via Settings → Members.
  • On the test user's next request (page or API): auth() returns null, withAuth rejects, page redirects to /login.
  • Issue an API key as the test user, then have an admin remove them. Confirm the API key returns 401 on the next call (the ApiKey row is gone).
  • If the deployment uses MCP/OAuth (e.g., Claude Desktop), revoke a user with an active OAuth token and confirm subsequent MCP calls 401.
  • Re-add the test user via invite → they sign in cleanly with a fresh JWT carrying the bumped sessionVersion.
  • Confirm pre-existing sessions for other users are unaffected (no incidental version bumps).
  • leaveOrg — same cascade behavior when a non-owner leaves voluntarily.
  • leaveOrg — last-owner guard still rejects the action.
  • Existing JWT cookies issued before this PR shipped continue to work (backwards compat — they have no sessionVersion claim and fall back to 0).

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Automatic session invalidation: when a user is removed or leaves an organization, their active sessions, OAuth tokens, and API keys are atomically revoked and take effect on their next request.
    • Audit actions: added organization membership lifecycle audit events for member added, removed, and left.

…moval
Adds a per-user `sessionVersion` integer to the `User` model. The version is
baked into every newly-minted JWT cookie via the `jwt` callback, copied onto
the session via the `session` callback, and verified on every read by a
wrapped `auth()` function that compares the cookie's claim against the
current DB value — mismatch returns null, treating the session as logged out
on the very next request.
Backwards compatible: pre-migration cookies have no claim and fall back to 0,
which matches the default User.sessionVersion of 0, so existing sessions
keep working until something explicitly bumps the user's version.
The `auth()` wrapper is memoized per-request via React `cache()` so the
extra DB read happens at most once per request even though `auth()` is
called from many places (layout, page, withAuth, getAuthenticatedUser).
`removeMemberFromOrg` and `leaveOrg` now run three credential-revocation
helpers inside the existing serializable transaction:
- `invalidateAllSessionsForUser` — bumps the version, killing every active
JWT cookie for the user on their next request.
- `revokeUserOAuthTokens` — deletes their `OAuthToken`,
`OAuthRefreshToken`, and `OAuthAuthorizationCode` rows. Not org-scoped
because OAuthClient has no `orgId`.
- `revokeUserApiKeysInOrg` — deletes their `ApiKey` rows scoped to the
current org (ApiKey.orgId).
Net effect: when an admin removes a member (or a member leaves), the user's
JWT cookie, personal API keys for that org, and OAuth tokens all stop
working atomically. A failed transaction rolls back all four changes.
@github-actions

This comment has been minimized.

@coderabbitai

coderabbitaiBot commented May 2, 2026

Copy link
Copy Markdown
Contributor

Walkthrough

Adds per-user sessionVersion (DB + Prisma) used in JWTs; NextAuth propagates and validates the claim via a cached auth() that returns null on mismatch. removeMemberFromOrg/leaveOrg now increment sessionVersion and revoke a user’s OAuth tokens and org API keys inside the same Prisma transaction. CHANGELOG and mocks updated.

Changes

Per-User JWT Session Versioning & Membership Revocation

Layer / File(s)Summary
Data Shape / Migration
packages/db/prisma/migrations/20260501170139_add_user_session_version/migration.sql, packages/db/prisma/schema.prisma
Adds sessionVersion Int @default(0) to User and creates a DB column with default 0.
Type Augmentation
packages/web/src/auth.ts
Augments next-auth and next-auth/jwt types: Session, User, and JWT include sessionVersion?: number.
Auth Core: JWT & Session Propagation
packages/web/src/auth.ts
Credentials provider returns sessionVersion; jwt callback writes token.sessionVersion; session callback copies token.sessionVersion into session.
Auth Wiring: cached auth() Validation
packages/web/src/auth.ts
NextAuth result is stored in nextAuthResult; exports handlers, signIn, signOut; new auth = cache(async () => ...) calls nextAuthResult.auth(), loads user from DB and returns null if DB sessionVersion ≠ JWT sessionVersion.
Membership Flows & Revocation (transactional)
packages/web/src/features/userManagement/actions.ts
removeMemberFromOrg and leaveOrg now, inside the same Serializable Prisma transaction, call helpers that increment the user’s sessionVersion, delete OAuth tokens/authorization codes/refresh tokens, and delete org API keys before deleting the membership record.
Helpers / Revocation Implementation
packages/web/src/features/userManagement/actions.ts
Adds invalidateAllSessionsForUser(prisma, userId), revokeUserOAuthTokens(prisma, userId), and revokeUserApiKeysInOrg(prisma, userId, orgId) that perform the DB deletes/updates via the transaction client.
Mocks & Changelog
packages/web/src/__mocks__/prisma.ts, CHANGELOG.md
Mock user gets sessionVersion: 0; CHANGELOG documents org membership audit-actions and session versioning behavior.

Sequence Diagram

sequenceDiagram
participant Client
participant NextAuth as NextAuth<br/>(middleware)
participant Auth as auth()<br/>(cache)
participant DB as Database
participant API as Protected<br/>Route
Client->>NextAuth: Request with JWT cookie (sessionVersion)
NextAuth->>Auth: call auth()
Auth->>DB: fetch user by sub
DB-->>Auth: user (sessionVersion = X)
alt X == JWT.sessionVersion
Auth-->>NextAuth: session object
NextAuth->>API: forward request (authorized)
API-->>Client: 200 OK
else mismatch
Auth-->>NextAuth: null
NextAuth-->>Client: 401 Unauthorized
end
Loading

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly Related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check nameStatusExplanation
Title check✅ PassedThe title 'feat(web): JWT session versioning and credential revocation on org removal' accurately describes the main changes: JWT session versioning and credential revocation on user removal/org departure.
Docstring Coverage✅ PassedNo functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch brendan/jwt-versioning

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@CHANGELOG.md`:
- Around line 10-11: The changelog entry currently under "### Added" describing
per-user JWT session versioning is a fix, not a new feature; move the entire
bullet ("Added per-user JWT session versioning so admin-driven member
removals... [`#1168`](https://github.com/sourcebot-dev/sourcebot/pull/1168)") out
of the "### Added" section and append it to the bottom of the "### Fixed"
section, preserving the exact text and PR link and leaving other entries/order
unchanged.
In `@packages/web/src/features/userManagement/actions.ts`:
- Around line 130-161: The new revocation functions revokeUserApiKeysInOrg and
revokeUserOAuthTokens perform unindexed deleteMany queries; add appropriate
indexes to avoid full-table scans by updating the Prisma schema: add a composite
index on ApiKey for (createdById, orgId) and add single-column indexes on
OAuthToken.userId, OAuthRefreshToken.userId, and OAuthAuthorizationCode.userId
(or composite if you prefer specific access patterns), then generate and apply a
migration so the deleteMany calls run against indexed columns within the
transaction.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 21dca91d-f46d-4355-a889-59c628f6d5d5

📥 Commits

Reviewing files that changed from the base of the PR and between ff41d83 and 5b9b3ed.

📒 Files selected for processing (6)
  • CHANGELOG.md
  • packages/db/prisma/migrations/20260501170139_add_user_session_version/migration.sql
  • packages/db/prisma/schema.prisma
  • packages/web/src/__mocks__/prisma.ts
  • packages/web/src/auth.ts
  • packages/web/src/features/userManagement/actions.ts

Comment threadCHANGELOG.md
Comment threadpackages/web/src/features/userManagement/actions.ts
@brendan-kellam
brendan-kellam merged commit 7243fdf into mainMay 4, 2026
8 checks passed
@brendan-kellam
brendan-kellam deleted the brendan/jwt-versioning branch May 4, 2026 18:17
@github-actionsgithub-actionsBot mentioned this pull request May 4, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@brendan-kellam
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(web): JWT session versioning and credential revocation on org removal - #1168

Merged
brendan-kellam merged 4 commits into
mainfrom
brendan/jwt-versioning
May 4, 2026
Merged

feat(web): JWT session versioning and credential revocation on org removal#1168
brendan-kellam merged 4 commits into
mainfrom
brendan/jwt-versioning

Conversation

@brendan-kellam

@brendan-kellambrendan-kellam commented May 2, 2026

Copy link
Copy Markdown
Contributor

This PR adds a sessionVersion integer to the User table and JWT token. Whenever auth is called, we compare the version stored in the database and the token. If they don't match, then the session is invalid (null). This allows us to invalidate JWT tokens from the server by incrementing the sessionVersion for a given user.

We use this capability in the user removal path to sign out users when the admin removes them from the organization.

Test plan

  • Sign in as a test user, hit /api/repos, confirm 200.
  • Have an admin remove the test user via Settings → Members.
  • On the test user's next request (page or API): auth() returns null, withAuth rejects, page redirects to /login.
  • Issue an API key as the test user, then have an admin remove them. Confirm the API key returns 401 on the next call (the ApiKey row is gone).
  • If the deployment uses MCP/OAuth (e.g., Claude Desktop), revoke a user with an active OAuth token and confirm subsequent MCP calls 401.
  • Re-add the test user via invite → they sign in cleanly with a fresh JWT carrying the bumped sessionVersion.
  • Confirm pre-existing sessions for other users are unaffected (no incidental version bumps).
  • leaveOrg — same cascade behavior when a non-owner leaves voluntarily.
  • leaveOrg — last-owner guard still rejects the action.
  • Existing JWT cookies issued before this PR shipped continue to work (backwards compat — they have no sessionVersion claim and fall back to 0).

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Automatic session invalidation: when a user is removed or leaves an organization, their active sessions, OAuth tokens, and API keys are atomically revoked and take effect on their next request.
    • Audit actions: added organization membership lifecycle audit events for member added, removed, and left.

…moval
Adds a per-user `sessionVersion` integer to the `User` model. The version is
baked into every newly-minted JWT cookie via the `jwt` callback, copied onto
the session via the `session` callback, and verified on every read by a
wrapped `auth()` function that compares the cookie's claim against the
current DB value — mismatch returns null, treating the session as logged out
on the very next request.
Backwards compatible: pre-migration cookies have no claim and fall back to 0,
which matches the default User.sessionVersion of 0, so existing sessions
keep working until something explicitly bumps the user's version.
The `auth()` wrapper is memoized per-request via React `cache()` so the
extra DB read happens at most once per request even though `auth()` is
called from many places (layout, page, withAuth, getAuthenticatedUser).
`removeMemberFromOrg` and `leaveOrg` now run three credential-revocation
helpers inside the existing serializable transaction:
- `invalidateAllSessionsForUser` — bumps the version, killing every active
JWT cookie for the user on their next request.
- `revokeUserOAuthTokens` — deletes their `OAuthToken`,
`OAuthRefreshToken`, and `OAuthAuthorizationCode` rows. Not org-scoped
because OAuthClient has no `orgId`.
- `revokeUserApiKeysInOrg` — deletes their `ApiKey` rows scoped to the
current org (ApiKey.orgId).
Net effect: when an admin removes a member (or a member leaves), the user's
JWT cookie, personal API keys for that org, and OAuth tokens all stop
working atomically. A failed transaction rolls back all four changes.
@github-actions

This comment has been minimized.

@coderabbitai

coderabbitaiBot commented May 2, 2026

Copy link
Copy Markdown
Contributor

Walkthrough

Adds per-user sessionVersion (DB + Prisma) used in JWTs; NextAuth propagates and validates the claim via a cached auth() that returns null on mismatch. removeMemberFromOrg/leaveOrg now increment sessionVersion and revoke a user’s OAuth tokens and org API keys inside the same Prisma transaction. CHANGELOG and mocks updated.

Changes

Per-User JWT Session Versioning & Membership Revocation

Layer / File(s)Summary
Data Shape / Migration
packages/db/prisma/migrations/20260501170139_add_user_session_version/migration.sql, packages/db/prisma/schema.prisma
Adds sessionVersion Int @default(0) to User and creates a DB column with default 0.
Type Augmentation
packages/web/src/auth.ts
Augments next-auth and next-auth/jwt types: Session, User, and JWT include sessionVersion?: number.
Auth Core: JWT & Session Propagation
packages/web/src/auth.ts
Credentials provider returns sessionVersion; jwt callback writes token.sessionVersion; session callback copies token.sessionVersion into session.
Auth Wiring: cached auth() Validation
packages/web/src/auth.ts
NextAuth result is stored in nextAuthResult; exports handlers, signIn, signOut; new auth = cache(async () => ...) calls nextAuthResult.auth(), loads user from DB and returns null if DB sessionVersion ≠ JWT sessionVersion.
Membership Flows & Revocation (transactional)
packages/web/src/features/userManagement/actions.ts
removeMemberFromOrg and leaveOrg now, inside the same Serializable Prisma transaction, call helpers that increment the user’s sessionVersion, delete OAuth tokens/authorization codes/refresh tokens, and delete org API keys before deleting the membership record.
Helpers / Revocation Implementation
packages/web/src/features/userManagement/actions.ts
Adds invalidateAllSessionsForUser(prisma, userId), revokeUserOAuthTokens(prisma, userId), and revokeUserApiKeysInOrg(prisma, userId, orgId) that perform the DB deletes/updates via the transaction client.
Mocks & Changelog
packages/web/src/__mocks__/prisma.ts, CHANGELOG.md
Mock user gets sessionVersion: 0; CHANGELOG documents org membership audit-actions and session versioning behavior.

Sequence Diagram

sequenceDiagram
participant Client
participant NextAuth as NextAuth<br/>(middleware)
participant Auth as auth()<br/>(cache)
participant DB as Database
participant API as Protected<br/>Route
Client->>NextAuth: Request with JWT cookie (sessionVersion)
NextAuth->>Auth: call auth()
Auth->>DB: fetch user by sub
DB-->>Auth: user (sessionVersion = X)
alt X == JWT.sessionVersion
Auth-->>NextAuth: session object
NextAuth->>API: forward request (authorized)
API-->>Client: 200 OK
else mismatch
Auth-->>NextAuth: null
NextAuth-->>Client: 401 Unauthorized
end
Loading

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly Related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check nameStatusExplanation
Title check✅ PassedThe title 'feat(web): JWT session versioning and credential revocation on org removal' accurately describes the main changes: JWT session versioning and credential revocation on user removal/org departure.
Docstring Coverage✅ PassedNo functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch brendan/jwt-versioning

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@CHANGELOG.md`:
- Around line 10-11: The changelog entry currently under "### Added" describing
per-user JWT session versioning is a fix, not a new feature; move the entire
bullet ("Added per-user JWT session versioning so admin-driven member
removals... [`#1168`](https://github.com/sourcebot-dev/sourcebot/pull/1168)") out
of the "### Added" section and append it to the bottom of the "### Fixed"
section, preserving the exact text and PR link and leaving other entries/order
unchanged.
In `@packages/web/src/features/userManagement/actions.ts`:
- Around line 130-161: The new revocation functions revokeUserApiKeysInOrg and
revokeUserOAuthTokens perform unindexed deleteMany queries; add appropriate
indexes to avoid full-table scans by updating the Prisma schema: add a composite
index on ApiKey for (createdById, orgId) and add single-column indexes on
OAuthToken.userId, OAuthRefreshToken.userId, and OAuthAuthorizationCode.userId
(or composite if you prefer specific access patterns), then generate and apply a
migration so the deleteMany calls run against indexed columns within the
transaction.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 21dca91d-f46d-4355-a889-59c628f6d5d5

📥 Commits

Reviewing files that changed from the base of the PR and between ff41d83 and 5b9b3ed.

📒 Files selected for processing (6)
  • CHANGELOG.md
  • packages/db/prisma/migrations/20260501170139_add_user_session_version/migration.sql
  • packages/db/prisma/schema.prisma
  • packages/web/src/__mocks__/prisma.ts
  • packages/web/src/auth.ts
  • packages/web/src/features/userManagement/actions.ts

Comment threadCHANGELOG.md
Comment threadpackages/web/src/features/userManagement/actions.ts
@brendan-kellam
brendan-kellam merged commit 7243fdf into mainMay 4, 2026
8 checks passed
@brendan-kellam
brendan-kellam deleted the brendan/jwt-versioning branch May 4, 2026 18:17
@github-actionsgithub-actionsBot mentioned this pull request May 4, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@brendan-kellam
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

feat(web): JWT session versioning and credential revocation on org removal - #1168

Merged
brendan-kellam merged 4 commits into
mainfrom
brendan/jwt-versioning
May 4, 2026
Merged

feat(web): JWT session versioning and credential revocation on org removal#1168
brendan-kellam merged 4 commits into
mainfrom
brendan/jwt-versioning

Conversation

@brendan-kellam

@brendan-kellambrendan-kellam commented May 2, 2026

Copy link
Copy Markdown
Contributor

This PR adds a sessionVersion integer to the User table and JWT token. Whenever auth is called, we compare the version stored in the database and the token. If they don't match, then the session is invalid (null). This allows us to invalidate JWT tokens from the server by incrementing the sessionVersion for a given user.

We use this capability in the user removal path to sign out users when the admin removes them from the organization.

Test plan

  • Sign in as a test user, hit /api/repos, confirm 200.
  • Have an admin remove the test user via Settings → Members.
  • On the test user's next request (page or API): auth() returns null, withAuth rejects, page redirects to /login.
  • Issue an API key as the test user, then have an admin remove them. Confirm the API key returns 401 on the next call (the ApiKey row is gone).
  • If the deployment uses MCP/OAuth (e.g., Claude Desktop), revoke a user with an active OAuth token and confirm subsequent MCP calls 401.
  • Re-add the test user via invite → they sign in cleanly with a fresh JWT carrying the bumped sessionVersion.
  • Confirm pre-existing sessions for other users are unaffected (no incidental version bumps).
  • leaveOrg — same cascade behavior when a non-owner leaves voluntarily.
  • leaveOrg — last-owner guard still rejects the action.
  • Existing JWT cookies issued before this PR shipped continue to work (backwards compat — they have no sessionVersion claim and fall back to 0).

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Automatic session invalidation: when a user is removed or leaves an organization, their active sessions, OAuth tokens, and API keys are atomically revoked and take effect on their next request.
    • Audit actions: added organization membership lifecycle audit events for member added, removed, and left.

…moval
Adds a per-user `sessionVersion` integer to the `User` model. The version is
baked into every newly-minted JWT cookie via the `jwt` callback, copied onto
the session via the `session` callback, and verified on every read by a
wrapped `auth()` function that compares the cookie's claim against the
current DB value — mismatch returns null, treating the session as logged out
on the very next request.
Backwards compatible: pre-migration cookies have no claim and fall back to 0,
which matches the default User.sessionVersion of 0, so existing sessions
keep working until something explicitly bumps the user's version.
The `auth()` wrapper is memoized per-request via React `cache()` so the
extra DB read happens at most once per request even though `auth()` is
called from many places (layout, page, withAuth, getAuthenticatedUser).
`removeMemberFromOrg` and `leaveOrg` now run three credential-revocation
helpers inside the existing serializable transaction:
- `invalidateAllSessionsForUser` — bumps the version, killing every active
JWT cookie for the user on their next request.
- `revokeUserOAuthTokens` — deletes their `OAuthToken`,
`OAuthRefreshToken`, and `OAuthAuthorizationCode` rows. Not org-scoped
because OAuthClient has no `orgId`.
- `revokeUserApiKeysInOrg` — deletes their `ApiKey` rows scoped to the
current org (ApiKey.orgId).
Net effect: when an admin removes a member (or a member leaves), the user's
JWT cookie, personal API keys for that org, and OAuth tokens all stop
working atomically. A failed transaction rolls back all four changes.
@github-actions

This comment has been minimized.

@coderabbitai

coderabbitaiBot commented May 2, 2026

Copy link
Copy Markdown
Contributor

Walkthrough

Adds per-user sessionVersion (DB + Prisma) used in JWTs; NextAuth propagates and validates the claim via a cached auth() that returns null on mismatch. removeMemberFromOrg/leaveOrg now increment sessionVersion and revoke a user’s OAuth tokens and org API keys inside the same Prisma transaction. CHANGELOG and mocks updated.

Changes

Per-User JWT Session Versioning & Membership Revocation

Layer / File(s)Summary
Data Shape / Migration
packages/db/prisma/migrations/20260501170139_add_user_session_version/migration.sql, packages/db/prisma/schema.prisma
Adds sessionVersion Int @default(0) to User and creates a DB column with default 0.
Type Augmentation
packages/web/src/auth.ts
Augments next-auth and next-auth/jwt types: Session, User, and JWT include sessionVersion?: number.
Auth Core: JWT & Session Propagation
packages/web/src/auth.ts
Credentials provider returns sessionVersion; jwt callback writes token.sessionVersion; session callback copies token.sessionVersion into session.
Auth Wiring: cached auth() Validation
packages/web/src/auth.ts
NextAuth result is stored in nextAuthResult; exports handlers, signIn, signOut; new auth = cache(async () => ...) calls nextAuthResult.auth(), loads user from DB and returns null if DB sessionVersion ≠ JWT sessionVersion.
Membership Flows & Revocation (transactional)
packages/web/src/features/userManagement/actions.ts
removeMemberFromOrg and leaveOrg now, inside the same Serializable Prisma transaction, call helpers that increment the user’s sessionVersion, delete OAuth tokens/authorization codes/refresh tokens, and delete org API keys before deleting the membership record.
Helpers / Revocation Implementation
packages/web/src/features/userManagement/actions.ts
Adds invalidateAllSessionsForUser(prisma, userId), revokeUserOAuthTokens(prisma, userId), and revokeUserApiKeysInOrg(prisma, userId, orgId) that perform the DB deletes/updates via the transaction client.
Mocks & Changelog
packages/web/src/__mocks__/prisma.ts, CHANGELOG.md
Mock user gets sessionVersion: 0; CHANGELOG documents org membership audit-actions and session versioning behavior.

Sequence Diagram

sequenceDiagram
participant Client
participant NextAuth as NextAuth<br/>(middleware)
participant Auth as auth()<br/>(cache)
participant DB as Database
participant API as Protected<br/>Route
Client->>NextAuth: Request with JWT cookie (sessionVersion)
NextAuth->>Auth: call auth()
Auth->>DB: fetch user by sub
DB-->>Auth: user (sessionVersion = X)
alt X == JWT.sessionVersion
Auth-->>NextAuth: session object
NextAuth->>API: forward request (authorized)
API-->>Client: 200 OK
else mismatch
Auth-->>NextAuth: null
NextAuth-->>Client: 401 Unauthorized
end
Loading

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly Related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check nameStatusExplanation
Title check✅ PassedThe title 'feat(web): JWT session versioning and credential revocation on org removal' accurately describes the main changes: JWT session versioning and credential revocation on user removal/org departure.
Docstring Coverage✅ PassedNo functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch brendan/jwt-versioning

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@CHANGELOG.md`:
- Around line 10-11: The changelog entry currently under "### Added" describing
per-user JWT session versioning is a fix, not a new feature; move the entire
bullet ("Added per-user JWT session versioning so admin-driven member
removals... [`#1168`](https://github.com/sourcebot-dev/sourcebot/pull/1168)") out
of the "### Added" section and append it to the bottom of the "### Fixed"
section, preserving the exact text and PR link and leaving other entries/order
unchanged.
In `@packages/web/src/features/userManagement/actions.ts`:
- Around line 130-161: The new revocation functions revokeUserApiKeysInOrg and
revokeUserOAuthTokens perform unindexed deleteMany queries; add appropriate
indexes to avoid full-table scans by updating the Prisma schema: add a composite
index on ApiKey for (createdById, orgId) and add single-column indexes on
OAuthToken.userId, OAuthRefreshToken.userId, and OAuthAuthorizationCode.userId
(or composite if you prefer specific access patterns), then generate and apply a
migration so the deleteMany calls run against indexed columns within the
transaction.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 21dca91d-f46d-4355-a889-59c628f6d5d5

📥 Commits

Reviewing files that changed from the base of the PR and between ff41d83 and 5b9b3ed.

📒 Files selected for processing (6)
  • CHANGELOG.md
  • packages/db/prisma/migrations/20260501170139_add_user_session_version/migration.sql
  • packages/db/prisma/schema.prisma
  • packages/web/src/__mocks__/prisma.ts
  • packages/web/src/auth.ts
  • packages/web/src/features/userManagement/actions.ts

Comment threadCHANGELOG.md
Comment threadpackages/web/src/features/userManagement/actions.ts
@brendan-kellam
brendan-kellam merged commit 7243fdf into mainMay 4, 2026
8 checks passed
@brendan-kellam
brendan-kellam deleted the brendan/jwt-versioning branch May 4, 2026 18:17
@github-actionsgithub-actionsBot mentioned this pull request May 4, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@brendan-kellam