Fix OIDC redirect_uri_mismatch, invalid_grant, and invalid_client
A fast troubleshooting runbook for developers dealing with OIDC sign-in failures after deploys, domain changes, proxy changes, or secret rotation. You’ll get a decision path, exact commands, and concrete fixes for redirect URI mismatch, invalid_grant, and invalid_client without wading through protocol basics.
TL;DR — Most OIDC sign-in failures come down to one of three things: the app is sending a different
redirect_urithan the identity provider has registered, the authorization code is being redeemed with the wrong client credentials, or the code/verifier/session is stale or mismatched. Start by capturing the exact/authorizerequest in your browser, compareredirect_uribyte-for-byte with the IdP registration, then verify the token exchange payload and client auth method. Reading time: ~6 min
The scenario
It’s Tuesday afternoon. You merged a harmless-looking change: new public domain, a proxy tweak, or a secret rotation in your deployment system. Five minutes later, users click "Sign in" and bounce back to your app with error=invalid_grant, or the IdP shows redirect_uri_mismatch, or your backend logs invalid_client from the token endpoint. The app worked this morning, the deploy is green, and now every login path is broken except maybe your local machine.
View as text
[Browser] ---- 1. GET /login ----> [App] [App] ---- 2. 302 /authorize?redirect_uri=... ----> [IdP] [IdP] ---- 3. 302 code=... ----> [Browser] [Browser] ---- 4. GET /callback?code=... ----> [App] [App] ---- 5. POST /token ----> [IdP] [IdP] <---- 6. tokens or error ---- [App]
Symptoms
- Browser or IdP-hosted error page shows one of these verbatim:
redirect_uri_mismatchThe redirect_uri is not valid for this clientinvalid_grantinvalid_clientunauthorized_client
- Your app callback URL gets hit with query params like:
GET /oidc/callback?error=access_denied&error_description=... HTTP/1.1
GET /oidc/callback?code=SplxlOBeZQQYbYS6WxSbIA&state=... HTTP/1.1
- Backend logs during token exchange:
POST https://idp.example.com/oauth2/token -> 400
{"error":"invalid_grant","error_description":"Code verifier invalid"}
POST https://idp.example.com/oauth2/token -> 401
{"error":"invalid_client","error_description":"Client authentication failed"}
- Reverse proxy/app logs show the app constructing the wrong callback URL:
redirect_uri=https://internal-app:3000/oidc/callback
redirect_uri=http://app.example.com/oidc/callback
redirect_uri=https://www.example.com/auth/callback
curl -Iagainst the callback endpoint shows an unexpected redirect or scheme change:
curl -I https://app.example.com/oidc/callback
HTTP/2 301
location: http://app.example.com/oidc/callback
server: nginx
Likely causes
| Cause | How common | Quick check |
|---|---|---|
| Registered redirect URI does not exactly match what the app sends | Very common | In browser devtools, copy the /authorize URL and inspect its redirect_uri query param |
| App builds callback URL with wrong scheme/host/path because proxy headers or base URL are wrong | Very common | curl -I https://your-app.example.com/callback |
| Client secret/ID is wrong, rotated, or mounted from the wrong environment | Common | `grep -E 'OIDC_(CLIENT_ID |
Token endpoint auth method mismatch (client_secret_basic vs client_secret_post) | Common | `curl -s https://idp.example.com/.well-known/openid-configuration |
Authorization code reused, expired, or redeemed with a different redirect_uri/PKCE verifier | Common | Search app logs for the same authorization code= appearing twice |
Callback/session affinity issue across replicas causing lost state or PKCE verifier | Less common | `kubectl logs deploy/your-app |
| Clock skew causing code/token validation edge failures | Less common | timedatectl status |
Step-by-step diagnosis
-
Capture the exact authorize request.
- Browser devtools → Network → click the request to the IdP
/authorizeendpoint → copy full URL. - Problem signal:
redirect_uri=is not the public callback you intended, or differs by scheme, host, path, trailing slash, port, or URL encoding. - Jump to: Fixes → Redirect URI does not exactly match or Fixes → App builds callback URL incorrectly.
- Browser devtools → Network → click the request to the IdP
-
Check what your callback endpoint actually does over HTTP.
- Run:
curl -I https://app.example.com/oidc/callback
curl -I http://app.example.com/oidc/callback
- Problem signal: any unexpected
301/302, downgrade tohttp, redirect towwwvs apex, extra slash, or path rewrite. - Jump to: Fixes → App builds callback URL incorrectly.
- Compare the app config with the IdP registration.
- In your provider's dashboard, open the OIDC/OAuth application and inspect the allowed redirect/callback URLs. Then compare with your runtime config:
env | grep -E 'OIDC|OAUTH|AUTH|BASE_URL|PUBLIC_URL' | sort
- Problem signal: app sends
https://app.example.com/auth/callbackbut IdP hashttps://app.example.com/oidc/callback; or one side includes:443, trailing slash, or different subdomain. - Jump to: Fixes → Redirect URI does not exactly match.
- Verify client ID/secret and token auth method.
- Check discovery metadata:
curl -s https://idp.example.com/.well-known/openid-configuration | jq '{token_endpoint, token_endpoint_auth_methods_supported}'
- Then inspect app env or secret mount:
env | grep -E 'CLIENT_ID|CLIENT_SECRET|TOKEN_ENDPOINT_AUTH_METHOD' | sort
- Problem signal: token endpoint returns
401withinvalid_client, or your app usesclient_secret_postwhile the IdP expectsclient_secret_basic. - Jump to: Fixes → Client credentials are wrong or Fixes → Token endpoint auth method mismatch.
- Check whether the authorization code is stale, reused, or paired with the wrong verifier.
- Search logs around one failed login attempt:
grep -R "code=" /var/log/your-app* | tail -n 20
- Problem signal: same code redeemed twice, long delay between callback and token exchange, or log lines like
PKCE verifier missing,state mismatch,nonce mismatch,invalid_grant. - Jump to: Fixes → Authorization code/PKCE/session mismatch.
- If you run multiple replicas, test session stickiness/storage.
- For Kubernetes:
kubectl get pods -l app=your-app
kubectl logs deploy/your-app --since=15m | grep -E 'state mismatch|verifier|session'
- Problem signal: login starts on one pod and callback lands on another with in-memory session storage; verifier/state missing on callback.
- Jump to: Fixes → Authorization code/PKCE/session mismatch.
- Rule out clock skew.
- Run:
timedatectl status
- Problem signal: system clock off by more than a minute or NTP unsynchronized.
- Jump to: Fixes → Clock skew.
Fixes
Redirect URI does not exactly match what the IdP has registered
Update either the IdP registration or the app so the value matches exactly, byte-for-byte.
Common mismatches:
https://app.example.com/oidc/callbackvshttps://app.example.com/oidc/callback/https://app.example.com/callbackvshttps://www.app.example.com/callbackhttp://localhost:3000/callbackused in production- URL-encoded path differences after framework upgrades
Typical app config:
export OIDC_REDIRECT_URI="https://app.example.com/oidc/callback"
export APP_BASE_URL="https://app.example.com"
If your app derives the callback from a base URL, pin the public base URL instead of trusting request headers:
APP_BASE_URL=https://app.example.com
OIDC_REDIRECT_PATH=/oidc/callback
In your provider's dashboard, edit the OAuth/OIDC client and replace the allowed callback URL with the exact value from the browser /authorize request.
Verify it worked:
# Start login and confirm authorize request contains the exact registered URI
App builds callback URL incorrectly because proxy headers or base URL are wrong
If TLS terminates at a load balancer or reverse proxy, forward the original scheme/host and configure the app/framework to trust proxy headers.
nginx example:
location / {
proxy_pass http://app:3000;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Port $server_port;
}
If your framework requires explicit trust-proxy settings, enable them in app config. Also set a canonical public URL so redirects do not depend on incoming headers.
⚠️ Changing proxy behavior can affect all routes and sessions. Apply in staging first if the app is customer-facing.
Verify it worked:
curl -I https://app.example.com/oidc/callback
Expected shape:
HTTP/2 200
# or 405/400 from the app, but not a redirect to a different host/scheme/path
Client credentials are wrong, rotated, or loaded from the wrong environment
Check the actual runtime values, not just your secret manager UI.
env | grep -E 'CLIENT_ID|CLIENT_SECRET' | sort
If using Kubernetes:
kubectl describe deploy your-app | sed -n '/Environment:/,/Mounts:/p'
kubectl get secret your-app-secrets -o json | jq -r '.data | keys[]'
Rotate/update the secret in your deployment system, then restart the app so it picks up the new value.
kubectl rollout restart deploy/your-app
kubectl rollout status deploy/your-app
Verify it worked:
# New login attempt no longer returns 401 invalid_client from /token
Token endpoint auth method mismatch
Some IdPs expect client credentials in the Authorization: Basic ... header (client_secret_basic); others accept form fields (client_secret_post). Your client library may default to the wrong one.
Check discovery:
curl -s https://idp.example.com/.well-known/openid-configuration | jq '.token_endpoint_auth_methods_supported'
If your client supports explicit configuration, set it to one of the advertised methods.
Example generic config:
OIDC_TOKEN_ENDPOINT_AUTH_METHOD=client_secret_basic
To reproduce manually with Basic auth:
curl -sS -X POST https://idp.example.com/oauth2/token \
-u "$OIDC_CLIENT_ID:$OIDC_CLIENT_SECRET" \
-H 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'grant_type=authorization_code' \
--data-urlencode 'code=REDACTED' \
--data-urlencode 'redirect_uri=https://app.example.com/oidc/callback'
Verify it worked:
# Token exchange returns 200 with access_token/id_token instead of invalid_client
Authorization code reused, expired, or paired with the wrong PKCE verifier/session
invalid_grant often means the code was already used, expired before redemption, or the app lost the PKCE verifier/state between /authorize and /callback.
Actions:
- Retry with a fresh incognito session to rule out stale cookies.
- Ensure the callback is handled by the same session store as login initiation.
- If you run multiple replicas, stop using in-memory session storage for OIDC state/verifier.
Kubernetes quick mitigation if you currently store session state in-process:
kubectl scale deploy/your-app --replicas=1
⚠️ Scaling down can reduce capacity and cause user-visible impact. Use only as a short-lived diagnostic.
Longer-term fix: move session/state storage to shared storage such as Redis, or configure load balancer stickiness if your app requires it.
Also verify the token request uses the same redirect_uri as the authorize request and includes the correct code_verifier when PKCE is enabled.
Verify it worked:
# One fresh login completes; logs no longer show state mismatch, verifier missing, or invalid_grant
Clock skew
Sync system time on app hosts.
timedatectl set-ntp true
timedatectl status
If running containers, fix the node clock; containers inherit host time.
Verify it worked:
timedatectl status | grep 'System clock synchronized: yes'
Prevention
- Add a synthetic OIDC smoke test in CI against a non-production client.
# Fail build if authorize URL contains the wrong redirect_uri
APP_BASE_URL=https://preview.example.com \
node ./scripts/print-auth-url.js | grep -F 'redirect_uri=https%3A%2F%2Fpreview.example.com%2Foidc%2Fcallback'
- Pin the public base URL and callback path in config; do not derive them from request headers alone.
APP_BASE_URL=https://app.example.com
OIDC_REDIRECT_PATH=/oidc/callback
- Alert on token endpoint failures by error code.
# Example log query pattern
error="invalid_client" OR error="invalid_grant" OR message~"redirect_uri_mismatch"
- Add a startup check that logs effective OIDC settings with secrets redacted.
{"msg":"oidc config","issuer":"https://idp.example.com","client_id":"web-prod","redirect_uri":"https://app.example.com/oidc/callback","token_auth_method":"client_secret_basic"}
- Put proxy header checks in deployment validation.
curl -sI https://app.example.com/oidc/callback | awk 'BEGIN{IGNORECASE=1}/^location:/{print}'
# Expect no host/scheme rewrite here
- If you use PKCE/session state, store it in shared session storage before scaling horizontally.
# Example deployment policy check
kubectl get deploy your-app -o json | jq '.spec.replicas'
# If >1, verify session backend is shared, not memory-only
This article was written by an AI system and published pending human review. Verify anything you intend to act on.
Have a project in mind?
Get an instant AI price estimate for it, or talk directly to our team.
One email a month on what we learn building with AI