Refresh Token Reuse and Rotation Bugs in OAuth Flows
Access tokens are short-lived on purpose. Refresh tokens are the long-lived thing that quietly mints new access tokens in the background, which makes them the credential an attacker actually wants. The security question is simple to state: what happens when a refresh token is used twice?
Rotation and the reuse-detection contract
Modern guidance says public clients should use rotating refresh tokens. Each time you redeem a refresh token, the server issues a new one and invalidates the old. The payoff comes from what happens next: if an old (already-redeemed) refresh token ever shows up again, the server should assume theft and revoke the entire token family.
That's the contract you're testing. Two failure modes:
- No rotation. The same refresh token works forever. Steal it once, keep it alive indefinitely.
- Rotation without reuse detection. New tokens are issued, but replaying an old one either still works or silently fails without killing the family — so a thief and the victim can coexist.
The test, with one account
You only need your own account and the ability to watch the token endpoint (proxy the app's traffic).
- Log in, capture refresh token
R1. - Redeem
R1at the token endpoint. You get an access token and, if rotation is on,R2. - Redeem
R1again.
Now read the result carefully:
R1still returns fresh tokens → no rotation. Report it.R1fails, butR2keeps working normally → rotation is on but reuse detection isn't triggering family revocation. The attacker who grabbedR1is locked out, but so what — did anything invalidateR2?R1fails andR2is now dead, forcing re-login → this is the behavior you want to see. Reuse detection fired.
Model the theft scenario: you (the "attacker") redeem R1 after the legitimate client already rotated to R2. If the server doesn't then revoke the whole family, both parties keep valid sessions. That's the reportable gap.
Around the edges
A few adjacent checks tend to pay:
- Does logout actually revoke the refresh token server-side, or just drop it client-side?
- Does a password change or "sign out everywhere" kill outstanding refresh tokens?
- Are refresh tokens bound to the client (and ideally sender-constrained)? A refresh token that any client ID can redeem is weaker.
- Absolute lifetime — a rotating token with no maximum age still becomes a long-lived secret if the user stays active.
Keep everything to tokens you were issued. Don't try to guess or brute-force another user's refresh token; the finding is in the server's handling of your token, not in enumeration.
Reporting
Show the token endpoint requests and responses for the redeem-old-token sequence, redacting the token bodies but keeping enough to prove they differ. State plainly which contract broke: no rotation, or rotation without family revocation. Tie it to impact — a stolen refresh token grants persistent access that survives password changes, which is exactly what victims expect a password change to stop.
Recommend rotating refresh tokens for public clients, revoking the token family on reuse detection, honoring logout and password-reset revocation, and setting a sane absolute lifetime.
The IETF's OAuth 2.0 Security Best Current Practice (RFC 9700) is the authoritative source on refresh token rotation and reuse detection — quote the relevant section so triage grades the finding against a standard, not an opinion.