Skip to content

Revoke sessions when disabling user accounts - #236

Merged
jalexw merged 2 commits into
mainfrom
claude/admiring-heisenberg-eyz538
Sep 26, 2026
Merged

jalexw merged 2 commits into
mainfrom
claude/admiring-heisenberg-eyz538

Conversation

@jalexw

@jalexw jalexw commented Sep 26, 2026

Copy link
Copy Markdown
Contributor

Summary

When a user account is disabled, all of their existing sessions and tokens are now immediately revoked, not just prevented from logging in. Previously, a disabled account could continue using an existing refresh token cookie until it expired, maintaining full API and dashboard access.

Key Changes

  • Token revocation mechanism: Disabled accounts now have their tokens_valid_after watermark pinned to Number.MAX_SAFE_INTEGER, which revokes every token they hold regardless of when it was issued
  • Re-enable behavior: When re-enabling an account, the watermark is unpinned to the current time, ensuring pre-disable sessions stay revoked even after re-enabling (users must log in fresh)
  • Database migration: Migration 00041-disabled-users-revoke-tokens applies the watermark pin to accounts that were disabled before this change
  • Cache invalidation: The admin disable/enable endpoints now invalidate the cached tokens_valid_after watermark so revocation applies immediately rather than after the cache TTL
  • Comprehensive testing: Added unit tests for the setUserDisabled function and an E2E test verifying that disabled sessions are rejected at route guards and stay revoked after re-enabling

Implementation Details

  • The DISABLED_USER_TOKENS_VALID_AFTER constant (Number.MAX_SAFE_INTEGER) is defined in is-token-iat-revoked.ts and used consistently across the codebase and migrations
  • Disabling and re-enabling both happen in a single database transaction to ensure atomicity
  • Re-enabling only unpins the watermark if it's currently pinned (via a WHERE tokens_valid_after >= DISABLED_USER_TOKENS_VALID_AFTER condition), so re-enabling an account that wasn't disabled doesn't affect its sessions
  • The route guards, refresh token grant, and token introspection all already consult the tokens_valid_after watermark, so no changes to those surfaces were needed
  • Version bumps: auth-server 0.46.0 → 0.46.1, e2e-auth-tests 0.11.30 → 0.11.31

https://claude.ai/code/session_01VMPqKHYDEBEdEsHJL3BEQ9

… their live sessions

setUserDisabled() only flipped users.disabled. The route guards read
`disabled` from the token claims, so a disabled account kept full API and
dashboard access through its refresh-token cookie until it expired
(up to 14 days); only login and the refresh grant read the live row.

Disabling now pins the account's tokens_valid_after watermark to
DISABLED_USER_TOKENS_VALID_AFTER (Number.MAX_SAFE_INTEGER) in the same
transaction. That revokes every token the account holds, whatever its
iat, at every surface that already checks the watermark (route guards,
the refresh and authorization_code grants, introspection), including a
token minted by a request that raced the disable. The admin endpoint
drops the cached watermark so this applies immediately. Re-enabling
unpins it to the current time, so pre-disable sessions stay dead and the
user logs in again. Re-enabling an account that is not disabled leaves
its sessions alone.

Migration 00041 pins the watermark of accounts that were already
disabled, cutting off their sessions on deploy; its down() unpins them
to the current time.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01VMPqKHYDEBEdEsHJL3BEQ9
@jalexw jalexw self-assigned this Sep 26, 2026
…SAFE_INTEGER in migration 00041

The bare "9007199254740991" literal read like a timestamp. It is a
sentinel past any token's iat, so name it the way the app code does.
Same value; the sync test now checks the expression.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01VMPqKHYDEBEdEsHJL3BEQ9
@jalexw
jalexw merged commit 17cdf59 into main Sep 26, 2026
55 checks passed
@jalexw
jalexw deleted the claude/admiring-heisenberg-eyz538 branch September 26, 2026 18:35
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.

2 participants