invalid_grant
What each invalid_grant answer from the token endpoint means for an authorization code, a refresh token and a token exchange, and how to fix it.
On this page
Symptom#
The token endpoint answers with status 400 and a body like this:
{ "error": "invalid_grant", "error_description": "the authorization code is unknown, expired or already used" }invalid_grant means the thing you presented in exchange for tokens is no longer acceptable: a code, a refresh token or a subject token. The error_description says which case, and error_description is for you, not for the person signing in.
Likely causes#
Authorization code#
error_description | Cause | Fix |
|---|---|---|
| the authorization code is unknown, expired or already used | The code lives 60 seconds at most (the platform can shorten that) and works once. It was slow to redeem, was redeemed twice, or never existed (a copied or truncated value). | Exchange the code immediately and once. Start a new sign-in. |
| this authorization code has already been redeemed | The code was redeemed before. Identity treats a second redemption as a possible theft and revokes the tokens the first redemption created. | Find the duplicate call: a retry, a double click, or two instances handling the same callback. Start a new sign-in. |
| redirect_uri does not match the one this code was issued for | The token request sent a different redirect_uri from the one on the authorization request. | Send the same value, byte for byte. See Redirect URI mismatch. |
| code_verifier does not match the code_challenge | The PKCE verifier is not the one whose hash you sent in the authorization request. | Keep the verifier with the sign-in in progress, per person and per attempt. Do not generate a new one at the callback. |
| this authorization code is bound to a DPoP key the request did not prove | The client requires DPoP, and the token request was signed by a different key than the pushed authorization request. | Use the same key for both. See DPoP. |
| the session behind this authorization code is no longer valid | The person's session ended between the sign-in and the redemption. | Start a new sign-in. |
| the principal behind this grant no longer exists, or is not active | The account was closed or deactivated. | Nothing to fix in your code. |
| the tenant behind this grant is not active | The organization is not active. | Nothing to fix in your code. |
Refresh token#
A refused refresh always answers the refresh token is not valid, whatever the reason. That is deliberate: it does not tell a thief which part failed. The causes are:
- It was already used. Each use returns a new refresh token and retires the old one. Presenting a retired token is treated as theft after a grace window of up to 10 seconds, and then identity revokes the whole family of tokens and ends the session.
- It expired. A third-party client's refresh token lives at most 7 days sliding and 30 days absolute (the platform can shorten both), and never past the session's absolute limit.
- The session ended because the person signed out, used Sign out everywhere, changed a credential, or reached the idle or absolute limit.
- The person revoked the application under Connected apps, or a scope you hold was withdrawn.
- The account or its organization is no longer active.
Fix: store the newest refresh token every time you receive one, and serialize refreshes for one person so two requests do not use the same token. If it fails, send the person through a new sign-in.
A refresh token that is bound to a key and used with a proof from a different key gets invalid_dpop_proof rather than invalid_grant. See invalid_dpop_proof and use_dpop_nonce.
Token exchange#
error_description | Cause |
|---|---|
| subject_token could not be verified | The token is malformed, expired, revoked or not signed by this issuer. |
| subject_token was not issued to the calling client | The exchanging client is not the one the token was issued to. |
| subject_token is bound to a key or certificate this request did not prove | The exchange request must prove the key the subject token is bound to. |
| the subject token has no remaining lifetime to exchange | The token is about to expire. Exchange sooner. |
the subject token's av is no longer current | The caller's access changed after the token was issued. Ask for a new token. |
| the act chain would exceed depth 3 | The delegation chain is too long. |
| the subject token’s tdaid:agent_action is malformed | The token's agent action data is not valid. Ask for a new token. |
| the subject principal is no longer active, or the subject's tenant is not active | The caller or their organization was deactivated. |
The same descriptions apply to an actor_token. See Service to service.
A different answer: insufficient_user_authentication#
For an organization that requires single sign-on, a code redemption, a refresh or a token exchange can answer status 400 with insufficient_user_authentication instead, and the body carries acr_values and max_age. It means the session behind the grant has no recent sign-in through the organization's provider. Send the person through sign-in again with those values. This applies only where the platform has turned on sso_enforcement (default OFF).
What to check#
- Log the
error_descriptionand the time between the authorization response and the token request. - Check whether the callback can run twice: a browser retry, a prefetch, or two server instances.
- Compare the
redirect_uriof both requests. - Check you store a refresh token returned by every refresh.
- Check the session: is the person still signed in on Sessions and devices?
Fix#
Start a new sign-in for a code or a refresh token that is gone. For a bug in your own code, fix the cause above.