Refresh token theft
The copied access token in Token theft and replay stopped working within minutes. A refresh token is built to last. demo-refresh-token-4 is what lets the printer make your December calendar without you, and anyone else holding it could draw on the same authorization.
Why refresh tokens are worth stealing
A refresh token represents the whole authorization you gave a client, not one request to one API. It often lasts months, it needs no person to be present, and every use can produce a fresh access token with the grant's full scope. Current guidance calls refresh tokens an attractive target for exactly these reasons.
They are also kept for a long time, which gives an attacker time to find them. The printer's website keeps them, encrypted, in connection records in its database, as Server-rendered web applications described. The phone app keeps them on the device, where a careless backup or a compromised phone can expose them. A browser application that holds one puts it within reach of injected script.
What a thief can do with a copy depends on whether the client it belongs to can authenticate.
When the thief also needs the client
All domains, credentials, and tokens in these examples are fictional. Suppose an attacker gets far enough into the printer's systems to copy and decrypt its connection records, and tries one of the refresh tokens at the token endpoint:
POST /token HTTP/1.1
Host: auth.photos.example
Content-Type: application/x-www-form-urlencoded
grant_type=refresh_token
&refresh_token=demo-refresh-token-4
&client_id=photo-printer
The photo service issued that token to photo-printer, a confidential client. The original specification requires the authorization server to keep that binding and to check it, with client authentication, whenever the client can authenticate. This request carries no client credential, so the token endpoint answers invalid_client, and the refresh token alone is useless.
That is why the printer keeps its client credential somewhere other than its connection records, such as a secrets store, and why a private key held in a key management service helps further: it cannot be copied along with the database at all. A breach that reaches both turns into the incident Rotating client credentials described. The credential is revoked first, then the tokens issued with it are reviewed.
Racing under rotation
The printer's phone app, photo-printer-app, is a public client. It has no credential, so a copied refresh token is all a thief needs. For public clients, current guidance requires the authorization server either to bind refresh tokens to the client instance or to rotate them. With rotation, which The refresh token grant introduced, the server watches for one thing: a refresh token that has already been used, presented again.
Say the app holds demo-app-refresh-token-1, and a thief copies it from a phone backup. What happens next depends on who uses it first.
| Who refreshes first | What the server sees | Outcome |
|---|---|---|
| The thief | The thief receives demo-app-refresh-token-2. Later, the app presents demo-app-refresh-token-1, which was already used. | Reuse detected. The server revokes the family, including the thief's replacement. The thief keeps at most an access token until it expires, and you reconnect the app. |
| The app | The app receives demo-app-refresh-token-2. The thief then presents demo-app-refresh-token-1. | Reuse detected. The family is revoked, so the thief gets nothing, and the app must reconnect too. |
| The thief, while the app stays unused | An unbroken chain: token 1, then 2, then 3, each used once. | Nothing looks wrong. The thief keeps rotating until the app returns, the family reaches its absolute lifetime, or something else ends the grant. |
In the first two rows, the server cannot tell who presented the old token. It revokes everything, which stops the thief at the cost of one reconnection for you. The grace period that some servers allow, which Refresh tokens and rotation described, weakens these two rows slightly: a reuse within those few seconds is not treated as theft.
The third row is the gap that Refresh tokens and rotation anticipated when it capped every family with an absolute lifetime. Rotation notices theft only when both parties use the token, and an idle app uses nothing. The absolute lifetime ends the family on a fixed date however busily the thief rotates. The idle lifetime does not stop an active thief, but it does mean a token copied from an old phone or a forgotten backup is already dead by the time anyone finds it.
Sender-constraint closes the gap more directly. If the app's refresh tokens are bound to a key kept in the phone's secure hardware, a copy taken from a backup cannot be used at all, because the key was never in the backup. Advanced OAuth's DPoP lessons show how a public client binds its tokens this way.
Containing a theft
When the server detects reuse, or anyone suspects a theft, the response follows the same steps:
- Revoke the family, and the access tokens issued from the same grant where the server can reach them, as Token revocation described.
- Record a security event naming the client, the grant, the time, and the networks involved, but never the token values.
- Tell the person plainly, for example: "We disconnected Photo Printer from your photo account because one of its credentials was used in two places. Reconnect it if you still use it."
- Require reconnection. The client's next refresh fails with
invalid_grant, and, as The refresh token grant described, it marks the connection as needing attention and asks you to connect again.
Detected reuse is not the only reason to end a family. Refresh tokens and rotation listed the others, from a password reset or account recovery to a suspended client or a leaked client credential. Each one is also a way of containing a theft that nobody has noticed yet: a person who resets their password because something felt wrong should not leave a thief's refresh token working behind them.
Reuse detection depends on legitimate clients never causing it. A client that runs two refreshes for the same connection at once presents a used token just as a thief would, and the server cannot tell the difference. The one-refresh-at-a-time rule from The refresh token grant keeps the signal meaningful, so that when reuse is reported, it can be treated as the security event it usually is.