Revoke sessions when disabling user accounts - #236
Merged
Merged
Conversation
… 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
…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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
tokens_valid_afterwatermark pinned toNumber.MAX_SAFE_INTEGER, which revokes every token they hold regardless of when it was issued00041-disabled-users-revoke-tokensapplies the watermark pin to accounts that were disabled before this changetokens_valid_afterwatermark so revocation applies immediately rather than after the cache TTLsetUserDisabledfunction and an E2E test verifying that disabled sessions are rejected at route guards and stay revoked after re-enablingImplementation Details
DISABLED_USER_TOKENS_VALID_AFTERconstant (Number.MAX_SAFE_INTEGER) is defined inis-token-iat-revoked.tsand used consistently across the codebase and migrationsWHERE tokens_valid_after >= DISABLED_USER_TOKENS_VALID_AFTERcondition), so re-enabling an account that wasn't disabled doesn't affect its sessionstokens_valid_afterwatermark, so no changes to those surfaces were neededhttps://claude.ai/code/session_01VMPqKHYDEBEdEsHJL3BEQ9