The token endpoint returns invalid_grant for at least a dozen different reasons, from a reused authorisation code to a refresh token revoked by a password reset, and the bare error code never says which. RFC 6749 uses one code for any grant or refresh token that is invalid, expired, revoked, issued to another client or bound to a different redirect URI, and later specifications (PKCE, JWT bearer grants, refresh token rotation) added more causes under the same name. This guide works from the symptom to the cause: which grant failed, what the provider-specific detail says, where to look, the fix, and how to stop it recurring without weakening token security. It is for developers, identity engineers and platform teams running OAuth clients, including integrations and service accounts. Provider error texts were checked against Microsoft, Google and Okta documentation on 11 November 2026. Client authentication errors (invalid_client, unauthorized_client) have their own OAuth invalid_client and unauthorized_client Troubleshooting Guide.
Key takeaways
- Identify the grant type first.
invalid_granton an authorisation code exchange, a refresh, and a JWT bearer assertion have almost no causes in common. Thegrant_typein the failing request halves the search immediately. - The provider detail is the real error. Read
error_description, Entra's AADSTS number, Google'serror_subtypeor the Okta System Log event. They distinguish expiry, revocation, reuse and mismatch where the OAuth code cannot. - Many refresh failures are expected lifecycle events. Inactivity limits, password resets, admin revocation and Google's 100-token limit per account and client all end a grant by design. The fix is graceful re-authorisation, not a longer token.
- Concurrency causes a large share of rotation failures. Two workers refreshing the same rotated token at once looks exactly like token theft to the authorisation server, and can revoke the whole grant.
- Never "fix" invalid_grant by weakening controls. Disabling PKCE, rotation or reuse detection, or switching to a long-lived static key, turns an outage into an exposure.
What invalid_grant means, and what it does not
RFC 6749 section 5.2 returns invalid_grant (HTTP 400) when the authorisation grant or refresh token is invalid, expired or revoked, does not match the redirection URI used in the authorisation request, or was issued to another client. Later standards reuse it: RFC 7636 returns it when the PKCE code_verifier does not match the code_challenge, and RFC 7523 returns it for an invalid JWT used as an authorisation grant. Microsoft's own description is broader still: some of the authentication material (auth code, refresh token, access token or PKCE challenge) was invalid, unparseable, missing or otherwise unusable.
It is not a client authentication error. If the client's secret, certificate or assertion is wrong, the server returns invalid_client; if the client may not use the grant type, unauthorized_client. A wrong scope is invalid_scope. If you see one of those, you are on the wrong page.
Triage: narrow it down in five questions
- Which grant? Check
grant_type:authorization_code,refresh_token,urn:ietf:params:oauth:grant-type:jwt-bearer,urn:ietf:params:oauth:grant-type:token-exchangeor a device code. - What is the provider detail? Log the full JSON error body (never the token itself). Entra puts the AADSTS code and a trace ID in
error_description; Google addserror_subtypein some cases. - Did it ever work? A first-time failure points at configuration (redirect URI, PKCE, assertion claims). A failure after weeks of success points at lifecycle (expiry, revocation, rotation).
- How many users or instances? One user suggests that user's grant was revoked or expired. Every user at once suggests a deployment change, a key rotation or a server-side policy change.
- Is there concurrency? Multiple pods, threads, browser tabs or serverless invocations sharing one refresh token is the first suspect for intermittent refresh failures.
Authorisation code exchange failures
| Cause | Evidence | Fix | Prevent |
|---|---|---|---|
| Code already redeemed | Entra AADSTS54005 "OAuth2 Authorization code was already redeemed"; duplicate POSTs in app logs | Start a new authorisation request | Exchange the code once, server-side; guard against double form submits, retries on timeout and browser back. RFC 9700 requires codes to be invalidated after first use |
| Code expired | Delay between redirect and exchange; Entra AADSTS50089 "flow token expired" | New authorisation request | Exchange immediately. RFC 6749 recommends a maximum code lifetime of 10 minutes; many servers use far less |
| redirect_uri mismatch at the token endpoint | Value sent to /token differs from the authorisation request (scheme, port, trailing slash, encoding) | Send the identical value | One configuration constant for both calls. RFC 6749 4.1.3 requires the values to be identical |
| PKCE verifier mismatch | Entra AADSTS501481 or AADSTS50148; verifier from a different session, a stale cookie or a load-balanced instance without shared session state | Restart the flow | Store the verifier with the state value in the user's session; use S256; verifier 43 to 128 unreserved characters (RFC 7636) |
| Code issued to another client | Multiple app registrations or environments, code from staging exchanged with production credentials | Use the client that started the flow | Separate callback URLs per environment |
| Code corrupted in transit | Code contains characters altered by double URL-decoding or truncated by a proxy log limit | Decode once | Pass the code through unchanged; test with codes containing reserved characters |
Refresh token failures
A refresh that worked yesterday and fails today is usually a lifecycle event. Confirm which one before changing code.
| Cause | Provider evidence (documented) | Expected? | Action |
|---|---|---|---|
| Inactivity expiry | Entra AADSTS700082 "The refresh token has expired due to inactivity" and AADSTS70008; Google: unused for six months; Okta: expires after seven days of non-use with the default unlimited lifetime | Yes | Prompt for sign-in; for integrations, alert the owner before the idle window ends |
| Fixed lifetime reached | Entra AADSTS700084 for single-page apps (24-hour lifetime that cannot be extended); 90 days for other Entra scenarios | Yes | Re-run the authorisation code flow |
| Password changed or reset | Entra AADSTS50173 "The provided grant has expired due to it being revoked"; Google: password change when the token has Gmail scopes | Yes | Re-authorise; see revocation rules below |
| Revoked by user or administrator | User removed app access; admin "revoke sessions"; Google admin_policy_enforced when scopes are restricted | Yes | Re-authorise, and confirm the revocation was not a security response before re-granting |
| Session or sign-in frequency policy | Entra AADSTS70043 (Conditional Access sign-in frequency); Google Cloud session control returns invalid_grant with error_subtype invalid_rapt, sessions of 1 to 24 hours | Yes | Handle re-authentication gracefully; do not exempt the app from the policy |
| Token limit per account and client | Google: limit of 100 refresh tokens per Google Account per OAuth client ID; the oldest is invalidated silently | Design fault | Store and reuse one refresh token per user; do not request prompt=consent on every sign-in |
| Testing publishing status | Google: external apps in Testing status get refresh tokens that expire in 7 days | Design fault | Publish the app for production use |
| Rotated token reused | Okta System Log app.oauth2.as.token.detect_reuse or app.oauth2.token.detect_reuse; RFC 9700 says the server will revoke the active refresh token | Possible attack or race | Investigate first (next section) |
| Token binding failure | Entra AADSTS70000 with token binding header empty or hash mismatch | Configuration | Check the client's proof-of-possession implementation |
Revocation rules worth knowing
- Microsoft Entra: refresh tokens are bound to a user and client, not to a resource. A user changing their password revokes tokens obtained with password-based sign-in but not those from passwordless sign-in. An admin password reset in the Entra admin center also revokes confidential client tokens. Revoking all refresh tokens for a user revokes everything. B2B guest tokens are revoked in the home tenant, not the resource tenant.
- Entra token reuse: Microsoft says the platform does not revoke old refresh tokens when they are used, but apps should delete the old token after receiving a new one.
- Google: the six-month idle rule, password change with Gmail scopes, user revocation, time-based access ending and the 100-token limit all end the grant.
Rotation, concurrency and reuse detection
RFC 9700 (BCP 240, January 2025) requires refresh tokens for public clients to be sender-constrained or rotated. With rotation, each refresh issues a new refresh token and invalidates the old one; if an invalidated token is presented again, the server cannot tell the attacker from the legitimate client, so it revokes the active token and the client must obtain a new grant.
- Symptom of a race: intermittent
invalid_granton refresh after deploying more replicas, adding background sync, or opening the app in several tabs; failures cluster at access token expiry when every worker refreshes at once. - Diagnosis: correlate failure timestamps with refresh calls from different instances for the same user or integration. Two refreshes with the same token within seconds confirm it. Okta logs the reuse event.
- Fix: make refresh a single-flight operation per grant: one lock (distributed if needed), refresh, persist the new token atomically, then release. Other workers wait and read the new token.
- Grace periods: Okta allows a configurable grace period of 0 to 60 seconds (default 30) in which the previous token remains valid. It masks small races; it does not fix a design where many workers hold their own copy.
- When it is not a race: reuse from a different IP range, user agent or ASN than the legitimate client is a theft indicator. Treat it as an incident and see How to detect use of a stolen OAuth refresh token.
Rotation and expiry design choices are covered in the OAuth Refresh Token Rotation and Expiry Guide.
JWT bearer and assertion grants
Service accounts and server integrations often use a signed JWT as the grant (RFC 7523). Any validation failure returns invalid_grant: the aud must identify the authorisation server, the current time must be inside nbf and exp, and the signature must verify. Google documents these exact messages for its service account flow:
| Google error text | Cause | Fix |
|---|---|---|
| "Invalid JWT Signature." | Signing key deleted, disabled or expired, or not tied to the service account; assertion encoded with newlines or padding | Check the key ID against the service account's active keys; Base64url-encode without padding |
| "Invalid JWT: Token must be a short-lived token (60 minutes) and in a reasonable timeframe." | Signing host clock is wrong, or exp more than an hour after iat | Synchronise time (NTP); set exp no more than one hour after iat |
| "Not a valid email" or "Invalid email or User ID." | The user in sub (domain-wide delegation) does not exist | Correct the impersonated user |
For federated workload tokens exchanged at a cloud security token service, see the Multi-Cloud Workload Identity Troubleshooting Guide; for claim and signature checks in general, the JWT Validation Errors Troubleshooting Guide.
Choosing the right fix
| Situation | Right response | Wrong response |
|---|---|---|
| Interactive user app, grant expired or revoked | Catch invalid_grant, clear stored tokens, send the user through sign-in | Retrying the refresh in a loop (Entra AADSTS50196 detects client loops) |
| Unattended integration acting for a user | Alert the named owner to re-authorise; track idle and lifetime limits | Moving to a shared admin account with a non-expiring token |
| Server-to-server with no user | Use the client credentials grant or workload federation, which have no refresh token to lose | Keeping a refresh token from a person who has left |
| Rotation races in a scaled service | Single-flight refresh with shared storage | Disabling rotation or reuse detection |
| Small organisation, few integrations | Manual re-authorisation with an owner and calendar reminder can be enough | Ignoring failures until a business process breaks |
| Large estate | Central token broker or secrets platform, with failure telemetry per integration | Each team writing its own refresh logic |
For unattended workloads, see the Machine-to-Machine OAuth (Client Credentials) Guide.
Diagnosis workflow, testing and rollback
- Capture the failing request safely: method, endpoint,
grant_type, client ID, redirect URI and the full error body. Redact codes, tokens, verifiers and secrets. - Pull the server-side record: Entra sign-in logs by trace or correlation ID, Okta System Log, Google Cloud audit logs or the provider's equivalent. The server log usually states the reason the response hides.
- Classify: configuration (first-time, every user), lifecycle (after a period, one user), or concurrency (intermittent, scaled).
- Reproduce in a test tenant with the same client settings, then change one variable at a time.
- Deploy the fix behind a flag or to one instance first; keep the previous release ready to restore. Never roll back by disabling PKCE or rotation in production.
- Confirm the failure rate drops and no new reuse events appear for seven days.
Where identity fits
A refresh token held by an integration is a long-lived non-human credential acting with a human's authority. When the person leaves, changes password or revokes consent, the integration breaks, which is the correct outcome. Each integration needs a named owner, an inventory entry and monitoring of its refresh failures. Revocation is also a containment step: after the Salesloft Drift breach, where stolen OAuth tokens from one integration opened hundreds of Salesforce tenants, revoking the integration's tokens is exactly the action that produces invalid_grant for the attacker and the legitimate connection alike. Re-grant only after the cause is understood, using the OAuth Token Theft Incident Playbook.
Detection and measurement
- invalid_grant rate per client and grant type: a step change after a release points at configuration; a slow rise points at lifecycle.
- Refresh token reuse events: any Okta
detect_reuseevent or equivalent, investigated within a set time. - Integrations failing silently: unattended clients with repeated refresh failures and no successful call in 24 hours.
- Re-authorisation lead time: integrations within 14 days of an idle or lifetime limit.
- Time to restore: from first failure to working grant for business-critical integrations.
Common failure patterns
- Retry storms: treating
invalid_grantas transient and retrying it, which never succeeds and can trip loop detection. - Consent on every login: forcing a new refresh token each sign-in until the oldest are silently invalidated by a per-account limit.
- Shared refresh tokens across replicas: rotation and reuse detection then revoke the grant at random.
- Integrations owned by leavers: a grant tied to a departed employee's account stops on account disablement, with nobody to re-authorise.
- Logging tokens to debug: pasting codes and refresh tokens into tickets or logs, creating a credential exposure during the outage.
Framework mapping
| Framework | Reference | Relevance |
|---|---|---|
| NIST SP 800-53 Rev. 5 | IA-5 Authenticator Management; SC-23 Session Authenticity; AC-12 Session Termination; IA-11 Re-authentication | Token lifetimes, rotation and forced re-authentication |
| NIST CSF 2.0 | PR.AA-01; PR.AA-03; DE.CM-09 | Managing and authenticating service credentials, monitoring use |
| ISO/IEC 27001:2022 | A.8.5 Secure authentication | Secure token handling and re-authentication |
| MITRE ATT&CK | T1528 Steal Application Access Token; T1550.001 Application Access Token | Theft and replay that reuse detection and revocation counter |
| IETF | RFC 6749 s5.2; RFC 7636 s4.6; RFC 7523 s3.1; RFC 9700 s4.14 | Where invalid_grant is defined for each grant |
Practitioner checklist
- Log the full error body, AADSTS code,
error_subtypeand trace ID for every token endpoint failure, never the token. - Classify each failure by grant type before investigating.
- Treat
invalid_grantas permanent for that grant: no automatic retries. - Store the PKCE verifier with
statein shared session storage. - Use one constant for
redirect_uriin both authorisation and token requests. - Implement single-flight refresh and atomic token storage for scaled services.
- Give every user-delegated integration a named owner and alert before idle limits.
- Keep signing hosts on synchronised time for JWT assertion grants.
- Investigate refresh token reuse events as potential theft.
- Move unattended server workloads to client credentials or workload federation.
Standards and references
- IETF: RFC 6749 The OAuth 2.0 Authorization Framework
- IETF: RFC 9700 Best Current Practice for OAuth 2.0 Security
- IETF: RFC 7636 Proof Key for Code Exchange by OAuth Public Clients
- IETF: RFC 7523 JSON Web Token (JWT) Profile for OAuth 2.0 Client Authentication and Authorization Grants
- Microsoft Learn: Microsoft Entra authentication and authorization error codes
- Microsoft Learn: Refresh tokens in the Microsoft identity platform
- Google: Using OAuth 2.0 to Access Google APIs
- Google: Using OAuth 2.0 for Server to Server Applications
Related NHI Mgmt Group resources: OAuth Refresh Token Rotation and Expiry Guide · OAuth invalid_client and unauthorized_client Troubleshooting Guide · OAuth 2.0 and OpenID Connect Guide · How to detect use of a stolen OAuth refresh token