Skip to content

Bug: Refresh token rotation lacks reuse-detection — stolen token can fork session family indefinitely #565

@Srejoye

Description

@Srejoye

In apps/backend/src/routes/auth.ts, the POST /auth/refresh handler implements rotation: it marks the presented refreshToken as revokedAt and issues a new one in the same family. However, it never checks whether the presented token was already revoked before rotating.

Current flow:

  1. Load storedToken by tokenHash.
  2. If storedToken.revokedAt is set → return 401 "Refresh token revoked".
  3. Otherwise revoke it and issue a new token in the same family.

This looks correct at first glance, but there is no logic anywhere that revokes the entire family when a revoked token is reused. If an attacker steals a refresh token and uses it before the legitimate client does, both the attacker and the legitimate user end up with valid, divergent tokens in the same family — only the next user to present their (now-stale) token gets a 401 "Refresh token revoked", with no signal to the server that this indicates compromise. The legitimate user's session is silently broken while the attacker's forked session continues uninterrupted for up to 90 days (expiresAt: Date.now() + 90 * 24 * 60 * 60 * 1000).

There is no family-wide revocation (updateMany({ where: { family }, data: { revokedAt: ... } })) triggered on reuse-of-revoked-token detection, which is the standard mitigation for refresh token theft in rotating-token systems.

Affected: apps/backend/src/routes/auth.ts (POST /refresh handler, both around the storedToken.revokedAt check).

Metadata

Metadata

Assignees

Labels

Type

No type
No fields configured for issues without a type.

Projects

Status
Todo

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions