Home / Tutorials / Authentication

Authentication · Authentication · Auth blocked

Fix: invalid_grant: The provided authorization grant is invalid, expired, or revoked

Error invalid_grant: The provided authorization grant is invalid, expired, or revoked

By the Erzon engineering team · A step-by-step fix, not a forum thread

The invalid_grant error comes from an OAuth 2.0 token endpoint and means the grant you tried to exchange (an authorization code, a refresh token, or a signed assertion) was rejected. It is one error string covering many distinct failures, and providers deliberately keep it vague. The way through is to identify which grant type failed and check that grant’s specific rules: codes are single use and expire in minutes, refresh tokens get revoked and rotated, and assertions are time sensitive.

invalid_grant: The provided authorization grant is invalid, expired, or revoked

What this error means

In OAuth 2.0, your application trades a grant for tokens at the token endpoint. invalid_grant (defined in RFC 6749 section 5.2) is the endpoint saying the thing you presented is not currently exchangeable. For an authorization code, that means expired, already used, issued to a different client, or presented with a different redirect_uri (or PKCE verifier) than the one in the original authorization request. For a refresh token, it means revoked, expired, rotated away, or issued to a different client. For JWT-bearer assertions (service accounts), it usually means signature, audience, or clock problems.

Providers intentionally collapse all of these into one error to avoid leaking which part was wrong, so your logs, not the error string, have to carry the diagnostic weight.

Common causes

  • An authorization code exchanged twice: a double callback, a browser refresh on the redirect page, or a retry wrapper around the token request.
  • The code expired before exchange, often from a slow callback handler or a queued job doing the exchange later.
  • redirect_uri at the token request differing in any way from the authorization request, including trailing slashes and http versus https.
  • A PKCE code_verifier that does not match the code_challenge sent earlier.
  • A refresh token revoked by the user, invalidated by a password change, or expired by provider policy (Google testing-status apps expire refresh tokens after 7 days).
  • Refresh token rotation: the provider issued a replacement token and your app kept using, or failed to persist, the old or new one.
  • Clock skew on the server signing JWT assertions, or the wrong token endpoint audience.
  • Mixed credentials: exchanging a grant with a different client_id or secret than the one it was issued to, common with separate staging and production OAuth apps.

How to fix it

STABILIZE first. If a production integration is down because a refresh token died, re-run the authorization flow (or re-consent the affected account) to mint a fresh grant. That restores service; then find out why the token died so it does not repeat.

  1. Reproduce the failing exchange directly so you can see the exact request and full error body, which often includes an error_description with more detail than your library surfaces:
curl -s -X POST https://oauth2.example.com/token \
  -d grant_type=refresh_token \
  -d refresh_token="$REFRESH_TOKEN" \
  -d client_id="$CLIENT_ID" \
  -d client_secret="$CLIENT_SECRET"
  1. Check the boring physical causes before the interesting logical ones: server time and credential pairing.
timedatectl status | grep -E "synchronized|Local time"

Confirm the client_id in the failing request is the same application the grant was issued under, not its staging sibling.

DIAGNOSE by grant type. Authorization code failing: log the code’s issue and exchange timestamps (expired if minutes apart) and add idempotency on the callback route to expose double exchanges; diff every parameter of the authorization and token requests, because redirect_uri mismatches hide in trailing slashes. Refresh token failing: check the provider’s dashboard or token info endpoint for revocation, check whether the provider rotates refresh tokens, and audit whether your storage saved the newest one. Assertion failing: verify clock sync, the signing key, and the aud claim against the provider’s documented token endpoint.

FIX according to what you found. For rotation, persist the new refresh token atomically on every refresh response before using the access token:

resp = session.post(TOKEN_URL, data={
    "grant_type": "refresh_token",
    "refresh_token": stored.refresh_token,
    "client_id": CLIENT_ID,
    "client_secret": CLIENT_SECRET,
})
data = resp.json()
if resp.status_code == 400 and data.get("error") == "invalid_grant":
    mark_connection_needs_reauth(stored.user_id)  # do NOT retry; re-consent is required
else:
    resp.raise_for_status()
    stored.access_token = data["access_token"]
    if "refresh_token" in data:                    # rotation: always keep the newest
        stored.refresh_token = data["refresh_token"]
    stored.save()

Treat invalid_grant on refresh as terminal for that token: flag the connection for re-authorization instead of retrying, because retries cannot resurrect a revoked grant and some providers throttle repeated failures. For code exchanges, make the callback handler idempotent and exchange the code immediately in the request cycle, not in a background job that may run after expiry.

How to prevent it

  • Persist rotated refresh tokens transactionally, and treat a failed save as an incident, not a log line.
  • Exchange authorization codes immediately and guard the callback against double submission.
  • Keep server clocks NTP synced everywhere tokens or assertions are created.
  • Use distinct, clearly labeled OAuth clients per environment so staging credentials cannot meet production grants.
  • Monitor refresh failures per connection and alert on the first invalid_grant, so re-consent happens before users notice a dead integration.
  • For Google integrations, move the OAuth consent screen to production status so refresh tokens stop expiring on the 7-day testing clock.

When to call a senior engineer

Call for help when refresh tokens die repeatedly with no revocation you can find (rotation races between multiple workers are a classic and need distributed-lock analysis), when a provider migration or consent-screen change has invalidated a whole fleet of tokens, or when the integration needs redesigning to survive re-authorization gracefully at scale. Erzon engineers can audit the full token lifecycle in your code and storage, reproduce the exact failing exchange against the provider, and leave you with token handling that recovers automatically instead of paging you.

Related errors we fix

Still down after this?

Want a senior engineer to just fix it?

Paste this exact error into the form. A senior engineer replies within one business hour with what is likely wrong and a flat quote. Triage is free.

Book a fix

← Back to all error fixes

Still down?

Get a senior engineer on it now

Leave your email and a line about what is happening. A senior engineer replies within one business hour with what is likely wrong and a flat quote. Triage is free.

Or email [email protected]. Same engineers, same one-hour clock.