Auth Redirect Returns to the Wrong URL (redirect_uri_mismatch)

Sign-in redirects to localhost or the wrong domain, or the provider errors with redirect_uri_mismatch. Add the exact callback URI to the allowed list and drive it from env.

You click Google or GitHub sign-in in production, finish the consent screen, and the browser bounces back to http://localhost:3000/auth/callback (blank page), to some unrelated domain, or the provider stops you cold with Error 400: redirect_uri_mismatch. This is the single most common OAuth configuration bug: the redirect_uri your app sends doesn’t match an entry in the provider’s allowed list.

Fastest fix: click sign-in, open DevTools Network, copy the redirect_uri= value the provider received (URL-decode it), and paste that exact string into the provider’s allowed-callback list. For Google that is Cloud Console → APIs & Services → Credentials → your OAuth client → Authorized redirect URIs. Save, wait a few minutes for Google to propagate, retry in an incognito window. That clears the mismatch in most cases; the rest of this page is for when it doesn’t, and for stopping it from coming back.

First: how strictly does YOUR provider match?

Not every provider matches the same way, and the original advice “everything is byte-exact” is only true for some. Match your provider to its real behavior before you start editing entries — this is the bucket you’re in.

ProviderMatching behavior (as of June 2026)Wildcards / subpaths
Google OAuth 2.0Byte-exact: scheme, host, port, path, trailing slash all must matchNo wildcards; localhost may use any port, public URLs may not
GitHub OAuth AppHost + port exact; path must be a subdirectory of the registered callback; subdomains allowedNo *, but /callback/staging matches a /callback registration
Auth0Byte-exact per entry in Allowed Callback URLsNo wildcards in production tenants
ClerkByte-exact; if the URL isn’t registered, OAuth completes but no session is createdNative/custom-scheme URLs registered separately
Supabase AuthRedirect URLs support glob wildcards (*, **, ?); Site URL is the default fallback** globstar allowed; the underlying OAuth client URI is still exact

The practical takeaway: on Google, Auth0, and Clerk you are hunting for a one-character difference. On GitHub you have more slack (a single callback can cover sub-paths), and on Supabase you can use a wildcard for previews — but the OAuth client URI you register with Google/GitHub inside Supabase is still exact.

Common causes

Ordered by hit rate, highest first.

1. Production callback URL isn’t in the allowed list

Most common. You added http://localhost:3000/auth/callback during dev, deployed to prod, and never added https://yourdomain.com/auth/callback.

How to spot it: the provider error page shows redirect_uri_mismatch plus the URI it received. Compare that against the allowed list in the provider console.

2. Hardcoded localhost in code

const redirectUri = "http://localhost:3000/auth/callback"; // forever dev

That line never changes after deploy, so the prod request still sends localhost.

How to spot it: grep your codebase for a hardcoded localhost or a literal domain.

3. Scheme or trailing-slash mismatch

https://example.com/cb is not equal to https://example.com/cb/ (extra slash), and http:// is not equal to https://. On Google this fails outright.

How to spot it: DevTools → Network → inspect the redirect_uri= parameter and diff it against the allowed list character by character.

4. www vs apex domain

https://www.example.com/cb is not equal to https://example.com/cb. If your site serves both but the list only has one, requests from the other fail. (GitHub is the exception — it ignores sub-domains, so www and apex match the same callback.)

How to spot it: does your site do www to apex redirects, or the reverse?

5. Preview / staging URL not registered

Vercel previews use changing subdomains like myapp-git-feature-team.vercel.app. On byte-exact providers, OAuth will always fail in preview unless you handle it (see Step 4).

How to spot it: failing only on preview deployments, working on production.

6. Provider strips path or query

Some flows (older SAML / SSO) ignore the path and match only the origin. A ?from=signup query on the redirect URI gets dropped and you land on / instead of /auth/callback.

How to spot it: the redirect lands on / rather than your callback path.

Shortest path to fix

Step 1: Capture the exact redirect_uri the provider received

Click sign-in, open the Network tab, and find the request to the provider’s authorize endpoint:

https://accounts.google.com/o/oauth2/v2/auth?
  client_id=...&
  redirect_uri=https%3A%2F%2Fyourdomain.com%2Fauth%2Fcallback&
  ...

Copy the redirect_uri= value and URL-decode it (%3A becomes :, %2F becomes /). That decoded string is exactly what the provider sees and validates.

Step 2: Add it verbatim to the provider’s allowed list

Paste the decoded string, unchanged, into the right field. Current console paths as of June 2026:

Google Cloud Console:
  APIs & Services -> Credentials -> (your OAuth 2.0 Client ID) -> Authorized redirect URIs

GitHub:
  Settings -> Developer settings -> OAuth Apps -> (your app) -> Authorization callback URL

Auth0:
  Applications -> (your app) -> Settings -> Allowed Callback URLs

Supabase:
  Authentication -> URL Configuration -> Site URL + Redirect URLs

Clerk:
  Configure -> (SSO connection / OAuth application) -> Redirect URLs

Save. Google changes can take a few minutes and occasionally up to a few hours to propagate; GitHub, Auth0, Supabase, and Clerk are effectively instant.

Step 3: Drive base URL from env, never hardcode

// Next.js example
const baseUrl = process.env.NEXT_PUBLIC_SITE_URL; // never fall back to localhost
if (!baseUrl) throw new Error("NEXT_PUBLIC_SITE_URL not set");

const redirectUri = `${baseUrl}/auth/callback`;

Set it per-environment in your host platform (Vercel / Netlify / Railway):

Local (.env.local):    NEXT_PUBLIC_SITE_URL=http://localhost:3000
Preview:               NEXT_PUBLIC_SITE_URL=(dynamic — see Step 4)
Production:            NEXT_PUBLIC_SITE_URL=https://yourdomain.com

A common subtle bug: failing loudly on a missing env var (the throw above) is better than silently defaulting to localhost, which produces exactly the blank-localhost-page symptom in production.

Step 4: Handle preview deployments

Each Vercel preview has a different subdomain, so a fixed entry won’t cover them. Pick the approach that matches your provider:

Supabase (recommended for previews):
  Add a glob Redirect URL such as
  https://*-<your-team-slug>.vercel.app/**
  Supabase supports * ** and ? in Redirect URLs.

Google / Auth0 / Clerk (no wildcards):
  Use process.env.VERCEL_URL to build the per-deploy origin, then
  register ONE stable preview alias instead of every random subdomain:

  const baseUrl = process.env.VERCEL_URL
    ? `https://${process.env.VERCEL_URL}`
    : process.env.NEXT_PUBLIC_SITE_URL;

  Register a fixed alias like
  https://your-app-git-main-team.vercel.app/auth/callback

GitHub:
  One callback URL covers sub-paths and sub-domains, so a single
  https://your-app.vercel.app/auth/callback often suffices for staging.

Step 5: End-to-end verify in incognito

1. Open the production URL in a fresh incognito window
2. Click sign-in
3. Network tab: confirm the redirect_uri value is the prod URL
4. Complete the provider consent screen
5. Land back on your page, then check the session cookie is set
6. Refresh once and confirm the session persists

If any step fails, capture the console and network errors before changing anything else.

Step 6: Diff scheme / slash exactly

If it still fails, put the registered URI next to the one in the request and diff them literally:

echo "registered: https://example.com/auth/callback"   # from the provider dashboard
echo "in request: https://example.com/auth/callback/"  # from the Network tab (note slash)

On Google a one-character difference — a trailing slash, http vs https, a www — is enough to break it.

How to confirm it’s fixed

You are done when all three hold in a clean incognito session on the production domain:

  • The authorize request’s redirect_uri exactly matches a registered entry (no redirect_uri_mismatch).
  • After consent you land on your real callback path, not / or a blank localhost page.
  • The session cookie is set and survives a hard refresh.

If only preview deploys still fail while production works, you’re in cause 5 — revisit Step 4.

Prevention

  • Treat the allowed-redirect list as part of the deploy checklist: a new environment means a new redirect URI.
  • Ban hardcoded localhost and literal domains in code; add a CI lint rule that fails the build if it finds one.
  • Use one env var name across every environment (NEXT_PUBLIC_SITE_URL) so behavior is consistent.
  • Plan previews up front: a Supabase wildcard, or a single stable alias on byte-exact providers.
  • Always run a full sign-in flow on the production domain before calling a deploy done.
  • Annotate allowed-list entries (env, owner, date added) so audits are quick.
  • Periodically prune dead dev / staging URIs from the list — stale entries are an open redirect risk.

FAQ

Why does it work locally but fail in production? Your code or env var still resolves to http://localhost:3000, or you simply never added the production callback to the allowed list. Capture the redirect_uri in the Network tab on the production domain and compare it to what’s registered.

I added the URL but still get redirect_uri_mismatch on Google. Three usual reasons: Google hasn’t propagated yet (wait a few minutes, sometimes longer), you edited the wrong OAuth client ID, or there’s a character difference — most often a trailing slash or http vs https. Diff the two strings literally as in Step 6.

Does the trailing slash really matter? On Google, Auth0, and Clerk, yes — …/callback and …/callback/ are different entries. GitHub is more forgiving because it matches sub-paths of the registered callback.

Can I use a wildcard for Vercel preview URLs? Only on providers that support it. Supabase allows globs like https://*-team.vercel.app/** in Redirect URLs. Google, Auth0, and Clerk do not — register one stable preview alias instead.

OAuth succeeds but I’m logged out / have no session. The redirect landed somewhere your session code doesn’t run, or (on Clerk) the redirect URL wasn’t registered so the provider returned but no session was created. Confirm you land on the exact callback path and that the cookie is set in DevTools → Application → Cookies.

The provider drops my ?from=signup query parameter. Some flows match only the origin and strip path/query. Don’t carry state in the redirect URI; use the OAuth state parameter (or store intent server-side keyed by state) and read it back in the callback handler.

Tags: #Backend #Debug #Troubleshooting