Problem
refreshTokens() (src/modules/auth/auth.service.ts, lines 186–213) deletes the presented session row and mints a new pair — but:
- No reuse detection: when a stolen refresh token is presented AFTER the legitimate owner rotated it, the lookup simply finds nothing and returns 401. A proper rotation scheme treats reuse-of-already-rotated token as theft evidence and kills the whole session family. Here, whoever presents the (still-unexpired) token first wins and everyone else silently fails — the victim gets logged out but the attacker keeps everything if they were first, and neither event raises any alarm.
- No session-family concept: all sessions are flat rows; compromise of one device cannot be contained.
- Blocked users retain access:
findOrCreateUser() (lines 144–158) checks status === 'blocked' only when minting NEW tokens. Access tokens remain valid until natural expiry (ACCESS_TOKEN_EXPIRATION), and nothing validates user status on requests — blocking a malicious account mid-incident leaves it fully operational for the remaining token lifetime.
- Session rows accumulate forever — no TTL/cleanup job for expired
sessions entries.
Ground Rules
- Read context/architecture-context.md, context/code-standards.md, context/progress-tracker.md in full
- Read the full auth service and
src/jobs/nonce-cleanup/ for the established background-job pattern to mirror
- Keep response shapes backward-compatible
What To Build
- Add a
family_id (or parent-session link) to sessions; on refresh, mint into the same family. When a refresh token whose successor was already used is presented, revoke EVERY session in that family, emit a security audit event, and force re-authentication.
- Introduce a lightweight per-request (or cached-interval) status check for blocked users — e.g. short-TTL cache of user status consulted by
JwtAuthGuard strategy — balancing freshness against DB load; document the chosen staleness bound.
- Extend the existing jobs pattern with a session-cleanup job deleting expired rows.
- Tests: reuse triggers family-wide revocation; blocked user is denied within the documented staleness bound; cleanup job removes only expired rows.
Files To Touch
src/modules/auth/auth.service.ts
- migrations for sessions schema
src/common/guards/jwt-auth.guard.ts / jwt.strategy.ts
- new job module mirroring
src/jobs/nonce-cleanup
- tests
- relevant docs/progress tracker
Acceptance Criteria
Mandatory Checks Before Opening PR
Standard checklist applies. PRs failing any check will be closed without review.
Problem
refreshTokens()(src/modules/auth/auth.service.ts, lines 186–213) deletes the presented session row and mints a new pair — but:findOrCreateUser()(lines 144–158) checksstatus === 'blocked'only when minting NEW tokens. Access tokens remain valid until natural expiry (ACCESS_TOKEN_EXPIRATION), and nothing validates user status on requests — blocking a malicious account mid-incident leaves it fully operational for the remaining token lifetime.sessionsentries.Ground Rules
src/jobs/nonce-cleanup/for the established background-job pattern to mirrorWhat To Build
family_id(or parent-session link) to sessions; on refresh, mint into the same family. When a refresh token whose successor was already used is presented, revoke EVERY session in that family, emit a security audit event, and force re-authentication.JwtAuthGuardstrategy — balancing freshness against DB load; document the chosen staleness bound.Files To Touch
src/modules/auth/auth.service.tssrc/common/guards/jwt-auth.guard.ts/ jwt.strategy.tssrc/jobs/nonce-cleanupAcceptance Criteria
Mandatory Checks Before Opening PR
Standard checklist applies. PRs failing any check will be closed without review.