Fix/refresh token rotation grace period - #1264
Merged
ayomideadeniran merged 6 commits intoSep 3, 2026
Merged
Conversation
|
@Cent-Dave is attempting to deploy a commit to the Ayomide Adeniran's projects Team on Vercel. A member of the Team first needs to authorize it. |
|
@Cent-Dave Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits. You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀 |
|
@Cent-Dave PR under review — I will get back to you if I find any wrong implementations. |
…strict fail-closed handling
…ation to revokeFamily
ayomideadeniran
force-pushed
the
fix/refresh-token-rotation-grace-period
branch
from
September 3, 2026 12:23
7cb59a8 to
33c4dcf
Compare
| @@ -1,30 +1,31 @@ | |||
| import { Request, Response } from 'express'; | |||
| import { getCookieOptions, getRefreshTokenFromReq, REFRESH_TOKEN_COOKIE_NAME } from '../src/utils/cookie.js'; | |||
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.
closes #1106
fix(auth): implement refresh token rotation grace period and reuse detection
What changed
Implements a 10-second rotation grace window for concurrent refresh requests, immediate family-wide revocation on token reuse/theft detection, and cross-process distributed locking to make concurrency safety hold under real multi-instance deployments (not just single-process test runs).
backend/src/auth/token.service.ts: added token lineage tracking (familyId, tokenId per token), a 10-second grace window so in-flight concurrent requests using the immediately-previous token don't trigger false-positive lockouts, immediate family-wide revocation when a token is reused outside the grace window or an older ancestor token is presented, a Redis-based distributed lock (SET NX with a Lua compare-and-delete unlock script) so rotation is safe across multiple server processes/pods, not just within one Node process, and retry-with-backoff plus fail-closed-via-deletion in revokeFamily so a failed revocation write can never silently leave a compromised token family active.
backend/src/utils/redis.ts: supporting changes for the distributed lock and fail-closed handling.
backend/tests/auth.hardened-session.test.ts: expanded from an existing partially-failing 6-test file to 14 passing tests covering grace-window acceptance, post-grace-window rejection with family revocation, multi-generation ancestor reuse detection, cross-device logout revocation, family isolation, concurrent in-flight request handling, high-concurrency theft-event revocation, Redis-unreachable fail-closed behavior for both verify and rotate paths, and deterministic repeat-run stability.
Why
Prevented race conditions during concurrent refresh requests from causing accidental session lockouts, while ensuring genuine token theft triggers instant, reliable, universal revocation across all devices.
Design decisions
Grace window is 10 seconds per the issue spec; a request presenting the immediately-previous token within that window receives the current active pair instead of being rejected or triggering another rotation.
Concurrency safety is layered: an in-process Map deduplicates same-process races cheaply, while a Redis NX lock with a Lua-script atomic compare-and-delete unlock handles true cross-process/cross-pod races. The unlock script prevents one instance from ever releasing a lock it doesn't own, even after TTL expiry and reacquisition by another instance.
If the distributed lock cannot be acquired after retries, the code checks whether a competing instance already completed the rotation and the token is now legitimately in its grace window before failing; otherwise it fails closed and rejects.
All Redis-unavailable paths fail closed (reject the refresh) rather than falling back to trusting an unverified signed JWT.
revokeAllUserTokens relies solely on the always-in-sync rt:u: per-user index rather than a global keyspace scan, avoiding an O(n) scan of every active session in the system on every logout.
revokeFamily retries the revocation write up to 3 times with backoff; if it still can't persist the revoked state, it deletes the family key instead (which the verify/rotate paths already treat as revoked) and explicitly throws, so a failed revocation is never silently treated as successful.
Verification results
Targeted suite (auth.hardened-session.test.ts): 14 passed, 14 total. Re-run 5 consecutive times with 0 flakiness.
Full suite, before vs. after (node node_modules/jest/bin/jest.js --config jest.config.cjs, run from backend/):
Baseline (clean checkout): Test Suites: 32 failed, 1 skipped, 48 passed, 80 of 81 total. Tests: 43 failed, 2 skipped, 784 passed, 829 total.
After this change: Test Suites: 31 failed, 1 skipped, 49 passed, 80 of 81 total. Tests: 40 failed, 2 skipped, 795 passed, 837 total.
Net: -1 failed suite, +1 passed suite, -3 failed tests, +11 passed tests, 0 regressions elsewhere. The delta is fully explained by this file: 3 previously-failing tests in the old version of auth.hardened-session.test.ts now pass, and 8 new tests were added, all passing.
TypeScript compilation: token.service.ts compiles cleanly with zero type errors.
Pre-existing issues found (not fixed, not introduced by this change)
The remaining 31 failing test suites / 40 failing tests are pre-existing and unrelated to this change, confirmed present on a clean baseline checkout before any changes. They include: a syntax error in backend/src/routes/index.ts causing multiple suites to fail to parse, missing Prisma generated client (.prisma/client/default) breaking several suites that import the database layer, missing REDIS_URL environment variable breaking suites that import the export queue, several jest.mock() factories referencing out-of-scope variables (invalid under current Jest version), and unrelated failing assertions in students.routes.test.ts. These affect the broader test suite and predate this branch; worth a maintainer's attention separately from this fix.