Diagnosing 'invalid_grant' Errors in OAuth 2.0 Authorization Code Exchange
Learn why OAuth 2.0 returns 'invalid_grant' when exchanging an authorization code, how to spot the exact cause with a diagnostic table, and step‑by‑step checks to fix redirect URI mismatches, code reuse, expiration, or clock skew.
18 Feb 2026, 13:07 UTC

The Problem: Generic 'invalid_grant' Response
\nWhen exchanging an authorization code for an access token, the OAuth 2.0 server returns invalid_grant. This catch‑all error hides whether the code is expired, reused, or if request parameters differ.
Takeaway: Most invalid_grant cases stem from a mismatch or state violation between the browser‑leg authorization request and the server‑leg token request.
Cause‑Diagnostic Table
\n| Observed pattern | \nLikely cause | \nQuick verification | \n
|---|---|---|
| Works first try, fails on immediate retry | \nAuthorization code reused | \nSearch logs for duplicate POST /token with same code | \n
| Fails every attempt, even with fresh code | \nRedirect URI mismatch | \nCompare the exact redirect_uri string used in /authorize and /token requests | \n
| Fails after a delay of a few minutes | \nCode expired (short lifetime) | \nMeasure time between code receipt and token request; compare to provider’s code TTL (often 1‑10 min) | \n
| Intermittent failures across multiple app instances | \nClock skew between client and auth server | \nCheck server time against NTP; look for > 30 s drift | \n
Ordered Checks and Fixes
\n- \n
- \n Verify redirect_uri exactness\n
- \n
- Where to run: Inspect the browser network tab for the initial
/authorizerequest and your server logs for the/tokenPOST. \n - Permission: Read‑only access to logs or browser dev tools. \n
- Risk: None (read‑only). \n
- Check: The redirect_uri value must be an identical string, including scheme, host, port, path, and trailing slash. \n
- Fix: Use the pre‑registered URI constant in both legs; avoid building it from request headers. \n
\n - Where to run: Inspect the browser network tab for the initial
- \n Detect code reuse\n
- \n
- Where to run: Server application logs; look for two
POST /tokenentries with the samecodeparameter. \n - Permission: Log read access. \n
- Risk: None. \n
- Fix: Ensure the token‑exchange handler is invoked exactly once per code (e.g., remove retry loops, add a flag or lock). \n
\n - Where to run: Server application logs; look for two
- \n Validate code lifetime and clock sync\n
- \n
- Where to run: On the host that performs the token exchange, run
date -uand compare withcurl -s https://time.google.comor an NTP server. \n - Permission: Ability to execute date command and outbound NTP/HTTPS. \n
- Risk: Minimal; changing system time requires privileged access (not needed for check). \n
- Check: If the elapsed time between receiving the code and sending the token request exceeds the provider’s code TTL (commonly 60 s), the code is expired. \n
- Fix: Synchronize the system clock via NTP (
sudo ntpd -qgon Linux) and reduce processing latency. \n
\n - Where to run: On the host that performs the token exchange, run
Escalation Criteria
\n- \n
- If redirect URI, code reuse, and clock checks all pass yet
invalid_grantpersists, enable debug logging on the authorization server (if available) to capture internal error codes. \n - Contact the provider’s support with the exact request timestamps, client_id, and the redacted authorization code (first/last 4 chars) for further investigation. \n
- Consider rotating the client secret and re‑registering the redirect URI as a last resort. \n
Concrete Example: Manual Token Exchange with cURL
\nTo isolate application logic, perform a manual exchange using a freshly obtained code.
\ncurl -X POST https://auth.example.com/oauth/token\n -H "Content-Type: application/x-www-form-urlencoded"\n -d "grant_type=authorization_code"\n -d "code=CODE_FROM_REDIRECT"\n -d "redirect_uri=https://myapp.com/callback"\n -d "client_id=YOUR_CLIENT_ID"\n -d "client_secret=YOUR_CLIENT_SECRET"\n
Where to run: Any shell with network access to the auth server (client server or developer workstation).
\nPermissions: Ability to read the client secret (protect it; do not log).
\nRisks: Exposing the client secret in shell history or process list; mitigate by using a file with restricted permissions or a credential manager.
\nExpected check: A successful response returns JSON with access_token. If you receive invalid_grant, the problem lies in the request parameters or server state, not in your application code.
Limitations: This manual test does not reveal intermittent issues caused by load‑balancer sticky sessions or per‑instance clock drift; repeat the test on each backend host.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.