Internet Protocols · Identity Protocols
Refresh Token Rotation and Replay Detection in OAuth 2.0
How one-time-use refresh tokens turn a stolen credential into a detectable event, what the rotation chain costs in storage, and where concurrent use breaks the model.
An access token is short-lived because it is a bearer credential. The refresh token exists to avoid re-running the authorization flow every time the access token expires, and it necessarily lives longer. That makes it the most valuable credential in the system: a refresh token is a durable grant, usable from any network location, with no user interaction. Rotation is the mechanism that gives it a detectable failure mode.
The baseline: a long-lived bearer credential
RFC 6749 defines the refresh grant in a paragraph. The client posts grant_type=refresh_token with its refresh token, optionally narrowing the requested scope, and receives a new access token. The specification permits the authorization server to issue a new refresh token in the response, and permits it to keep the old one valid, but says nothing about requiring either behaviour.
The default behaviour of many implementations, until relatively recently, was to keep the refresh token valid indefinitely and never rotate it. The consequence is that a leaked refresh token yields access until someone notices and revokes it. Since the token is typically stored in a database, a file, or a browser’s storage, and since it produces access tokens silently, a leak can persist for the lifetime of the grant without generating any anomaly.
Rotation changes the economics. Each use of a refresh token consumes it and produces a successor. A leaked token is usable exactly once. If the legitimate client also uses it, one of the two uses arrives second and is by definition a replay, and the authorization server can treat that as evidence of compromise rather than as a client bug.
The chain and what the server must remember
The authorization server keeps, per grant, a record of the currently valid refresh token and enough history to recognise its predecessors. On a successful refresh, it issues a new token, marks the presented one as used, and links the new one to the same grant.
The minimum record for each token is its hash, its grant identifier, its state (active, used, revoked), and an expiry. Tokens themselves should not be stored in plaintext, for the same reason passwords are not: a read of the token table should not yield usable credentials. A cryptographic hash is sufficient, because verification is by recomputation, not by lookup of a reversible value.
The history requirement is what distinguishes rotation with replay detection from rotation alone. If the server forgets a token the moment it is used, a replay looks like an unknown token and is rejected with invalid_grant, indistinguishable from an expired or revoked token, and producing no signal. Keeping used tokens in a bounded history lets the server answer “this was valid and already used” and take action on the whole grant.
The history must be bounded, because otherwise it grows without limit for a long-lived grant that refreshes often. A chain of a few generations is enough to catch an interleaved attacker: if the attacker uses a stolen token and the legitimate client then uses the same token, the replay is detected at the second use. If the legitimate client uses first and the attacker second, the attacker’s use is the replay. Either ordering is caught within one generation.
What happens on detection
The standard response, described in RFC 9700, is to revoke the entire grant: the current refresh token and every access token derived from it. The reasoning is that the server cannot tell which of the two parties is the attacker, so it must invalidate the credential that both hold. The legitimate client is forced back through the authorization flow, which is an inconvenience; the attacker loses access, which is the point.
This is a deliberate choice of availability against security. The alternative, revoking only the presented token and letting the current one continue, assumes the legitimate client is the one that just succeeded, which is a guess with a 50% chance of protecting the attacker. Some deployments do take that path when the refresh is happening from a plausible client context, which requires additional signals such as client authentication or a bound key.
Detection without revocation is nearly useless. A server that logs a replay and continues leaves the attacker’s successor token valid, so the attacker simply continues with the new chain and the legitimate client’s next attempt also fails as a replay, producing more alerts and no resolution.
Concurrency is the failure mode that matters
The mechanism’s practical problem is that clients are not single-threaded. A browser application with two tabs, a mobile application that fires several requests on resume, and a server process with parallel workers all may attempt a refresh at the same moment with the same token.
Two requests, both valid, both presenting the same token. The first consumes it and receives a successor. The second arrives with a token that is now marked used, and a strict replay rule revokes the grant. The user is logged out because of a race that neither request caused maliciously.
Several mitigations exist, each with a cost.
A short grace window: the server accepts a second use of a just-consumed token within a few seconds and returns the same successor that the first use produced. This requires storing the mapping from consumed token to its successor for the duration of the window, and it means a stolen token replayed within the window succeeds. The window length is the size of the hole.
Client-side serialisation: the client guarantees only one refresh is in flight, and other callers wait for its result. This is correct and cheap, but it requires every code path in the client to cooperate, and it does not help across multiple processes or devices sharing a grant.
Family identifiers: the authorization server groups tokens into a family and treats a replay as revoking the family, but only after the successor has itself been used. A replay of a token whose successor has not yet been used suggests a race; a replay after the chain has advanced suggests an attacker working from an old copy. This distinction is not in any specification and is a heuristic.
The grace window is the most common compromise, and it is worth stating its shape precisely: the risk is bounded by the window and by the attacker’s ability to use the token within it, which requires the attacker to have stolen the token and to be racing the legitimate client.
Rotation and the scope of the grant
A rotated refresh token inherits the grant’s scope. RFC 6749 permits a refresh request to specify a narrower scope than the original grant, and requires the authorization server to reject a request for a broader one with invalid_scope. A client that wants less access can therefore narrow on refresh without a new authorization flow; a client that wants more must go back through consent.
This asymmetry is the reason refresh is not a general-purpose re-authorization mechanism. It is a way to obtain access tokens under an existing decision, not a way to change the decision.
There is a subtle interaction with rotation: if the client narrows scope on refresh, the successor token carries the narrowed scope, and the narrowing persists through the chain. A client that narrows by accident, for example by omitting the scope parameter’s full value, permanently reduces its own access until it re-authorizes. Implementations that treat an absent scope as “same as before” avoid this; implementations that treat it as “empty” do not.
Revocation and the end of a chain
RFC 7009 defines a revocation endpoint where a client can present a token and have it invalidated. The endpoint is specified to accept both access and refresh tokens, to return 200 even for an unknown token (so the response does not reveal whether the token existed), and to invalidate related tokens at the server’s discretion, which for a refresh token should mean the whole chain.
Revocation is client-initiated and voluntary. It is not a substitute for short access token lifetimes, because a client that has lost control of a token is not in a position to revoke it.
The complementary mechanism is introspection, which lets a resource server ask whether a token is active. For refresh tokens, the useful variant is server-side revocation triggered by replay detection, which requires the authorization server to be able to invalidate access tokens that are already issued. With opaque access tokens this is a table update. With self-contained tokens it is not possible without a revocation list or a shorter lifetime, which is one of the costs discussed in JWT access token validation.
Rotation also interacts with the client authentication scheme. A public client cannot authenticate at the token endpoint, so the only thing distinguishing the legitimate client from an attacker holding the refresh token is the token itself. A confidential client can add its secret, so a stolen refresh token alone is insufficient. The stronger option binds the token to a key the client holds, so the token is unusable without a proof computed over the request. The range of client authentication methods, and what each actually proves, is covered in client authentication methods.