Skip to content

Fix/refresh token rotation grace period - #1264

Merged
ayomideadeniran merged 6 commits into
StellarDevHub:mainfrom
Cent-Dave:fix/refresh-token-rotation-grace-period
Sep 3, 2026
Merged

Fix/refresh token rotation grace period#1264
ayomideadeniran merged 6 commits into
StellarDevHub:mainfrom
Cent-Dave:fix/refresh-token-rotation-grace-period

Conversation

@Cent-Dave

@Cent-Dave Cent-Dave commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

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.

@vercel

vercel Bot commented Aug 31, 2026

Copy link
Copy Markdown

@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.

@drips-wave

drips-wave Bot commented Aug 31, 2026

Copy link
Copy Markdown

@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! 🚀

Learn more about application limits

@adeniran19-maker

Copy link
Copy Markdown

@Cent-Dave PR under review — I will get back to you if I find any wrong implementations.

@ayomideadeniran
ayomideadeniran force-pushed the fix/refresh-token-rotation-grace-period branch from 7cb59a8 to 33c4dcf Compare September 3, 2026 12:23
@@ -1,30 +1,31 @@
import { Request, Response } from 'express';
import { getCookieOptions, getRefreshTokenFromReq, REFRESH_TOKEN_COOKIE_NAME } from '../src/utils/cookie.js';
@ayomideadeniran
ayomideadeniran merged commit fca52b0 into StellarDevHub:main Sep 3, 2026
3 of 10 checks passed
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.

[Auth & Identity] Fix Refresh Token Rotation & Concurrency Grace Period in Hardened Sessions

4 participants