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:

  1. Localhost vs production mismatch. Registered https://myapp.com/auth/callback but testing from http://localhost:3000/auth/callback.
  2. Trailing slash mismatch. https://myapp.com/auth/callback vs https://myapp.com/auth/callback/.
  3. HTTP vs HTTPS. Some providers require HTTPS in production; localhost gets a pass.
  4. Vercel preview URLs. Each PR has a unique URL like myapp-git-feature-x.vercel.app/auth/callback — not registered.
  5. 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)
  • 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:

  1. Code already used. OAuth authorization codes are one-time-use. If your callback runs twice (browser refresh, double-render), the second attempt fails.
  2. Code expired. Most codes expire in 5–10 minutes.
  3. Code stolen / replayed. Suggests an attack; rotate everything.
  4. 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 (profile vs profil)
  • 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/ssr handles 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_ID vs GITHUB_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=0 in 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:

  1. Cookie not set. SameSite or Secure attributes preventing cookie write.
  2. Wrong domain on cookie. Cookie set for auth.myapp.com but used at myapp.com.
  3. Server didn’t store the session. Token received but never persisted.
  4. 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/ssr and 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


Sources