Skip to content

[Security] Revoke the whole refresh token family on reuse detection #175

Description

@katayama8000

Follow-up from #173, which audited src/auth/ against the authorization-hardening SEPs listed in #170 and left this row open.

Current behaviour

Refresh token handling in src/auth/ already does the two main things right:

  • Rotation — a refresh token is single-use; redeeming it issues a new one
  • Client binding — a refresh token is bound to the client it was issued to

What is missing is reuse detection. Replaying an already-redeemed refresh token is rejected, but the rejection is local to that token: the rest of the token family stays valid.

Why that matters

Rotation exists so that a stolen refresh token is detectable. The detection signal is exactly the case above — the legitimate client and the attacker both present the same token, so one of them replays a token that was already redeemed. Today that replay is turned away and nothing else happens, so whichever party holds the current token keeps a working session. If that is the attacker, the theft is silent.

OAuth 2.1 / the Security BCP call for the authorization server to revoke the entire refresh token family when a reuse is detected.

Suggested implementation

  1. Give issued refresh tokens a family identifier that survives rotation.
  2. Keep redeemed tokens around (at least until family expiry) instead of deleting them outright, so a replay is distinguishable from an unknown token.
  3. On presentation of a token that is known-but-already-redeemed, revoke every token in that family — refresh and access — and return invalid_grant.
  4. Log the event; it is a genuine security signal, not routine noise.

Note the token store is in-memory and resets on restart (documented), so this is bounded work — no persistence layer is involved.

Scope

src/auth/tokenStore.ts and the /token handler in src/auth/oauthRoutes.ts. Wants tests for: normal rotation still works, a replayed token is rejected, and the sibling access/refresh tokens are dead afterwards.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions