OAuth / social login errors
Status: 🟩 COMPLETE Last updated: 2026-06-21 Plain-English tagline: The errors that show up when “Sign in with Google” (or GitHub, Apple, etc.) doesn’t work. Redirect URIs, scopes, callback handlers — the OAuth pain points decoded.
What this is
A reference for OAuth (and OAuth-adjacent) errors. Most apply across providers — the patterns are universal even if the specific error messages vary.
For background: OAuth and social login đźź©, Authentication vs authorization đźź©, Custom auth vs Supabase Auth đźź©.
“redirect_uri_mismatch” / “invalid_redirect_uri”
What it means: The redirect URL your app sent in the OAuth request doesn’t match what the provider has registered for this app/client ID.
This is the #1 most common OAuth error.
Common causes:
- Localhost vs production mismatch. Registered
https://myapp.com/auth/callbackbut testing fromhttp://localhost:3000/auth/callback. - Trailing slash mismatch.
https://myapp.com/auth/callbackvshttps://myapp.com/auth/callback/. - HTTP vs HTTPS. Some providers require HTTPS in production; localhost gets a pass.
- Vercel preview URLs. Each PR has a unique URL like
myapp-git-feature-x.vercel.app/auth/callback— not registered. - Wildcards forbidden. Most providers don’t support wildcards in redirect URIs; you must list each one explicitly.
Fix:
- In the provider’s developer console (Google Cloud, GitHub OAuth Apps, etc.):
- Add EVERY redirect URI your app uses:
http://localhost:3000/auth/callback(dev)https://your-domain.com/auth/callback(prod)https://your-app.vercel.app/auth/callback(Vercel default)
- Add EVERY redirect URI your app uses:
- For Vercel previews: either use the Vercel system env var to know the URL, OR test on a stable preview branch with a registered URI.
- For Supabase Auth: the redirect URI is
https://<project-ref>.supabase.co/auth/v1/callback— register THIS with the provider, not your app URL. Supabase forwards to your app after.
”invalid_client” / “Client authentication failed”
What it means: The client ID or client secret you’re sending doesn’t match what the provider expects.
Common causes:
- Wrong client ID (copy-paste error)
- Wrong client secret
- Client secret expired or rotated
- Using the wrong environment’s credentials (dev secrets in prod, etc.)
Fix:
- Verify in the provider’s developer console that the client ID + secret match what your app is using.
- For Supabase: Supabase Dashboard → Authentication → Providers → click the provider — verify the Client ID and Client Secret fields.
”invalid_grant” / “Authorization code expired”
What it means: The authorization code you got back was rejected when you tried to exchange it for a token.
Common causes:
- Code already used. OAuth authorization codes are one-time-use. If your callback runs twice (browser refresh, double-render), the second attempt fails.
- Code expired. Most codes expire in 5–10 minutes.
- Code stolen / replayed. Suggests an attack; rotate everything.
- Wrong redirect URI in the exchange request. The redirect URI sent during token exchange must EXACTLY match the one used in the authorization request.
Fix:
- Make your callback handler idempotent — handle being called twice without crashing.
- In Next.js, callbacks should run only on the server-side route, not be invoked from client effects.
”access_denied” / “user_cancelled”
What it means: The user clicked “Cancel” or denied permission on the consent screen.
Fix: not an error per se — surface a friendly message to the user. “Looks like you cancelled the sign-in. Try again?"
"invalid_scope” / “insufficient_scope”
What it means: You requested a permission scope the provider doesn’t recognize or doesn’t allow for this client.
Common causes:
- Typo in scope name (
profilevsprofil) - Scope requires verification that hasn’t been completed (Google’s “restricted scopes” need app verification)
- Scope is only available on paid tiers of the provider
Fix:
- Check the provider’s scope reference for exact names.
- For sensitive scopes (read user emails, write to user data): expect a verification process. Google’s takes weeks.
”User declined access” but the user said they accepted
What it means: The provider’s consent screen ran but the response indicates denial.
Common causes:
- User declined a specific scope mid-consent (Google shows checkboxes for each)
- Browser plugin blocking the consent return
- Multiple OAuth tabs racing
Fix: test in a clean incognito session. If reproducible, file with the provider.
”PKCE verification failed”
What it means: Your OAuth flow uses PKCE (Proof Key for Code Exchange), and the code verifier sent during token exchange doesn’t match the code challenge sent during authorization.
Common causes:
- Different code_verifier values stored vs sent
- Code verifier lost between authorization and callback (session cleared)
- Race condition in storing/reading the verifier
Fix:
- For Supabase:
@supabase/ssrhandles PKCE correctly when configured. If you’ve custom-implemented, use the official helper instead. - For custom OAuth: store the code_verifier in an HTTP-only cookie or server-side session. Don’t rely on localStorage across tabs.
”OAuth2Strategy requires a clientID option” (Passport / NextAuth)
What it means: Your auth library wasn’t configured with the OAuth client credentials before being called.
Common causes:
- Missing env vars
- Config loaded after the strategy was registered
- Wrong env var names (
GITHUB_IDvsGITHUB_CLIENT_ID)
Fix:
- Verify env vars are loaded. Log them at startup (without printing secrets to public logs).
- Check the library’s config docs for exact variable names.
”Unable to verify the first certificate” (Node.js OAuth in dev)
What it means: Node.js couldn’t validate the TLS certificate of the OAuth provider (or your local callback URL).
Common causes:
- Corporate proxy with self-signed certs
- Local dev with a self-signed HTTPS cert
- Missing root CAs in Node’s trust store
Fix:
- For corporate proxy: add the proxy’s root CA to Node’s trust store via
NODE_EXTRA_CA_CERTS. - For local dev: use HTTP for localhost (providers generally allow it) instead of self-signed HTTPS.
- NEVER set
NODE_TLS_REJECT_UNAUTHORIZED=0in production. That disables certificate validation entirely.
”User session not found” after successful login
What it means: OAuth completed, user redirected back, but your app doesn’t see them as logged in.
Common causes:
- Cookie not set. SameSite or Secure attributes preventing cookie write.
- Wrong domain on cookie. Cookie set for
auth.myapp.combut used atmyapp.com. - Server didn’t store the session. Token received but never persisted.
- Session storage misconfigured (e.g. Redis URL wrong).
Fix:
- In DevTools → Application → Cookies: verify the auth cookie was set after the callback.
- For Supabase + Next.js: make sure you’re using
@supabase/ssrand the middleware/cookie helpers correctly — they handle session persistence. - For custom auth: verify the session was inserted into your DB and the cookie token references it.
”OAuth provider returned 500”
What it means: The provider’s own service had an error.
Fix: retry. If persistent, check the provider’s status page. Not your bug.
”Apple Sign In: invalid_client” specifically
What it means: Apple’s OAuth is uniquely picky. The JWT used as the client secret expires every 6 months max and has specific signing requirements.
Common causes:
- Client secret JWT expired
- Wrong key used to sign the JWT
- Wrong team ID / key ID embedded
Fix: regenerate the Apple client secret JWT following Apple’s exact specifications. Many libraries (NextAuth, etc.) provide helpers for this.
”Google OAuth: 403 access_denied: Access blocked: This app’s request is invalid”
What it means: Your Google OAuth app is in “Testing” mode and you (the signing-in user) aren’t on the test users list.
Fix:
- Add yourself as a test user in Google Cloud Console → OAuth consent screen → Test users.
- Or publish the app to production (requires verification for sensitive scopes).
”Token refresh failed” / “refresh_token expired”
What it means: A long-lived refresh token (used to obtain new access tokens) was rejected.
Common causes:
- Refresh token revoked (user signed out elsewhere, app uninstalled, etc.)
- Refresh token has its own expiry (some providers: months, others: never)
- Wrong client used for refresh (must match the original grant)
Fix:
- Re-authenticate. Direct the user to sign in again, freshly.
- Store refresh tokens securely (server-side only, never in browser).
See also
- Common errors — index 🟩
- Supabase errors 🟩 🟦 — including Supabase Auth JWT errors
- Browser errors đźź©
- OAuth and social login 🟩 — the textbook
- Authentication vs authorization đźź©
- Sessions and cookies đźź©
- JWT đźź©
- Secrets management đźź©
- Custom auth vs Supabase Auth đźź©
Sources
- OAuth 2.0 RFC 6749 — the canonical spec
- OAuth.com — Errors
- Supabase docs — Auth providers
- Google OAuth docs
- Apple Sign In docs