Skip to content

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

Open
brendan-kellam wants to merge 3 commits intomainfrom
brendan/jwt-versioning
Open

feat(web): JWT session versioning and credential revocation on org removal#1168
brendan-kellam wants to merge 3 commits intomainfrom
brendan/jwt-versioning

Conversation

@brendan-kellam
Copy link
Copy Markdown
Contributor

@brendan-kellam brendan-kellam commented May 2, 2026

This PR adds a sessionVersion column to the User table

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
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 2, 2026

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 name Status Explanation
Title check ✅ Passed The 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 ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check 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.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

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 thread CHANGELOG.md
Comment thread packages/web/src/features/userManagement/actions.ts
Sign up for free to 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