This refresh token has been revoked
This page helps you when refreshing a token fails and takes your app’s newest refresh token down with it, or when every refresh token of one user stops working at once, so the user has to sign in again.
What you see
Section titled “What you see”Your app refreshes, and the token endpoint answers 400:
{"error": "invalid_grant", "error_description": "This refresh token has been revoked."}Then the refresh token your app got from its previous refresh fails too, with the same “This refresh token has been revoked.” The audit log may hold a refresh_token_replay_detected event for your client.
Or every refresh token the user holds with your app fails at once, on every device, and the audit log may hold a revoked_consent_grants event naming the user and your client: the user’s consent was withdrawn. See A withdrawn consent.
Why it happens
Section titled “Why it happens”There are two causes: a refresh token that came back after it was rotated, which is the usual one, and a consent withdrawn.
A refresh token is good for one refresh. Each refresh returns a new one and retires the old one, and that’s called rotation.
When a retired refresh token comes back, the auth server can’t tell whether an attacker stole it or your own app sent it twice. So it assumes the worst: it revokes every live refresh token descended from the same sign-in, the newest one included. That’s what stops someone holding a stolen token from refreshing for as long as they like.
Your own app usually causes it in one of two ways:
- Parallel refreshes. Two threads, tabs or instances notice the access token has expired, and each refreshes with the same refresh token. One wins. The other is either refused, or, when it reads the token after the winner retired it, treated as a replay.
- Retrying after a lost response. The auth server rotated the token, but the response never reached your app. Retrying sends the retired token.
There’s no grace period for either: a retired token is retired the moment the refresh succeeds.
A withdrawn consent
Section titled “A withdrawn consent”When the user withdraws their consent to your app under Account, Consents, or an administrator revokes it on the user’s Consents tab, every refresh token the user holds with your app is revoked at once: the grants of every sign-in and every device, offline ones included. The user’s grants with other apps, and their session with the auth server, keep working.
There’s nothing to fix in your app beyond signing the user in again. If your app asks for consent, the user is asked again.
Fix it
Section titled “Fix it”This is for a refresh token sent twice.
- Serialize refreshes. Share one refresh between every thread, tab or instance that needs a token, with a lock or a single in-flight request the others wait for.
- Treat a lost response as “sign in again”, not “retry”. Without the response, your app doesn’t have the new refresh token, and the old one is retired.
Once a family is revoked, the user has to sign in to your app again. Their session with the auth server survives, so they usually go straight through.
What’s revoked, and what isn’t
Section titled “What’s revoked, and what isn’t”After a replay, only that one grant is revoked. The user’s session with the auth server, their sign-ins to other clients and their other grants keep working.
Access tokens already issued stay valid until they expire. They’re signed JWTs your APIs check on their own, so revoking the refresh token stops the next refresh, not an access token already handed out. Keep access token lifetimes short.
The replay check comes last. A refresh that fails for another reason first, such as a user who was disabled or whose password changed, gets that reason, and nothing is revoked.
The refresh_token_replay_detected event is written when the auth server revokes something, not on every repeat, and its first_refresh_token_jti identifies the grant. It doesn’t prove an attack: an app that refreshes in parallel triggers it too.
Other reasons a refresh fails
Section titled “Other reasons a refresh fails”These refreshes answer invalid_grant too:
error_description |
Why |
|---|---|
| “The refresh token is invalid because it has expired.” | The refresh token expired. An offline one expires after going unused for its idle timeout. |
| “The refresh token is invalid because it was superseded.” | The user’s password was changed or reset, or the user was disabled and has since been enabled again. |
| “The refresh token is invalid because the associated session has expired or been terminated.” | The session it was issued through ended, or the client was made public. |
| “The refresh token is invalid because it has expired (offline_access_max_lifetime).” | An offline refresh token reached its maximum lifetime. |
| “The refresh token is invalid.” | The user is disabled, or the token is unknown. So is a token that’s malformed or that none of the auth server’s signing keys verifies: one cut short or changed on its way, or one signed by a key that rotating the signing keys deleted. Rarely, a token issued while its family was being revoked. |
| “The user has either not given consent to this client or the previously granted consent has been revoked.” | The user’s consent was withdrawn (see A withdrawn consent), or it was never given, because Ask users for consent was turned on after the user signed in. Checked only for a refresh token from a sign-in: an offline one, any of a client that requires consent, and one renewing authserver:manage-account to any client but the admin console’s. Sign the user in again. |
| “Scope ‘authserver:manage-account’ is not recognized. The user has not consented to the ‘authserver:manage-account’ permission.” | The user’s consent leaves authserver:manage-account out: they unticked it on the consent screen. Send them through an ordinary sign-in to approve it, or refresh with a scope that leaves it out. |
| “The refresh token is invalid because it does not belong to the client.” | The request named a client other than the one the token was issued to. Each client refreshes only its own tokens. |