Repository navigation
fix(auth): revoke fallback JWT sessions via jti denylist (CS-01) - #4067
Merged
Merged
Conversation
Logout previously had no server-side effect on stateless fallback JWTs (issued when Redis is unavailable): revokeSession returned early for non-Redis tokens, so a leaked login JWT stayed valid for its full 30-day TTL. Fallback JWTs now carry a unique per-token jti (SHA-256 token digest for pre-rollout tokens) which revokeSession records in a denylist that lives until the token's own exp — always in-process on the serving instance, mirrored to Redis (revoked-jwt: keys, TTL = remaining lifetime) when reachable so other instances honor it, and re-mirrored lazily after an outage. verifySession rejects denylisted keys, and the logged-in fallback TTL drops from 30 days to a sliding 12 hours to bound the exposure window when revocation cannot propagate; Redis-backed sessions keep their 30-day TTL and deletion-based revocation unchanged.
The Redis mirror keys are 'revoked-jwt:<key>' under the session key prefix only in REDIS_HOST mode (client keyPrefix); REDIS_URL mode deliberately keeps unprefixed session keys, so the denylist keys are raw there. Align docs/AUTHENTICATION.md and the redis-session.ts comment with the actual key layout.
ety001
force-pushed
the
fix/cs-01-jwt-fallback-revocation
branch
from
October 19, 2026 15:50
1b53f34 to
dfd83f6
Compare
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.
Problem (audit finding CS-01, MEDIUM)
When Redis is unavailable,
createSessionissues a stateless JWT with a 30-day login-session TTL.revokeSessionreturned immediately for non-Redis token IDs whileverifySessionkept accepting any still-valid JWT, so logout provided no server-side invalidation and a leaked fallback login JWT remained usable for up to 30 days. (JWT_SECRETstrength checks prevent weak-key forgery but confer no revocation capability.)Design
jti: fallback JWTs are now signed with a fresh randomjtion every signing (createSession). Thejtiis deliberately distinct from the sessionuid(which is preserved across login/logout rotations), so revoking a superseded token can never reject its replacement. Tokens minted before this change carry nojti; their revocation key is the SHA-256 digest of the full token.lib/auth/jwt-denylist.ts, new):revokeSessionverifies the token's signature first (so forged input cannot stuff the denylist), then records the key with TTL equal to the token's remaining lifetime:revoked-jwt:keys under the existing session-store namespace (lib/auth/redis-session.tsgainedsetDenylistEntry/hasDenylistEntry), so other instances honor the revocation; an un-mirrored entry retries its mirror the next time its key is consulted, which propagates revocations made during an outage once Redis recovers.verifySessionrejects a fallback JWT whose key is denylisted (in-process + Redis mirror), treating it like an expired signature.TTL decision
Logged-in fallback JWT TTL drops from 30 days to 12 hours (
FALLBACK_PERSISTENT_JWT_TTL_SEC = 43200). Justification: these tokens are minted precisely when Redis is down, i.e. when revocation can only be honored per-instance — the shorter TTL bounds a leaked token's exposure to half a day even when revocation cannot propagate. The trade-off (a Redis outage longer than 12h logs fallback users out once) is acceptable next to an unrevokable 30-day credential. Active users are not disturbed: every session-touching request re-signs the fallback JWT (updateSession→createSession), so the 12-hour window slides while the token is in use. Challenge (anonymous) fallback JWTs keep their 600s TTL; Redis-backed sessions keep the documented 30-day TTL.Residual risk (documented in
docs/AUTHENTICATION.md)During a full Redis outage in a multi-instance deployment, revocation is only honored by the instance that served the logout until Redis recovers (lazy re-mirror on next consultation of the key, and new revocations mirror immediately). This is strictly better than the previous behavior where logout did nothing at all for fallback tokens, and the exposure is bounded by the 12-hour TTL. Production deployments should still configure
REDIS_URLfor immediate cluster-wide revocation.Test evidence
__tests__/lib/auth/jwt-denylist.test.ts(new, 12 tests) and extended__tests__/lib/auth/session.test.ts:revokeSession(token)→verifySession(token)returnsnull, while a concurrently issued token still verifies and the rotated post-logout token stays valid;exp(1 ms before → revoked; atexp→ not), mirror TTL equals the remaining lifetime (+1 s ceil slack at most);pnpm test: 86 files / 1082 tests passing.pnpm lint: unchanged baseline (7 warnings + 1 pre-existing error in__tests__/proxy.test.ts:383, untouched).tsc --noEmit: clean.