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
- Give issued refresh tokens a family identifier that survives rotation.
- Keep redeemed tokens around (at least until family expiry) instead of deleting them outright, so a replay is distinguishable from an unknown token.
- On presentation of a token that is known-but-already-redeemed, revoke every token in that family — refresh and access — and return
invalid_grant.
- 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.
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: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
invalid_grant.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.tsand the/tokenhandler insrc/auth/oauthRoutes.ts. Wants tests for: normal rotation still works, a replayed token is rejected, and the sibling access/refresh tokens are dead afterwards.