Skip to content

hard: refresh-token rotation lacks reuse detection and blocked users keep live sessions #119

Description

@EmeditWeb

Problem

refreshTokens() (src/modules/auth/auth.service.ts, lines 186–213) deletes the presented session row and mints a new pair — but:

  1. 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.
  2. No session-family concept: all sessions are flat rows; compromise of one device cannot be contained.
  3. 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.
  4. Session rows accumulate forever — no TTL/cleanup job for expired sessions entries.

Ground Rules

  1. Read context/architecture-context.md, context/code-standards.md, context/progress-tracker.md in full
  2. Read the full auth service and src/jobs/nonce-cleanup/ for the established background-job pattern to mirror
  3. Keep response shapes backward-compatible

What To Build

  1. 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.
  2. 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.
  3. Extend the existing jobs pattern with a session-cleanup job deleting expired rows.
  4. 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

  • Refresh-token replay revokes the entire session family and is observable in logs/events
  • Blocked wallets lose API access within a bounded, documented delay
  • Expired sessions garbage-collected
  • Suite green

Mandatory Checks Before Opening PR

Standard checklist applies. PRs failing any check will be closed without review.

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions