auth · tutorial

redirect_uri_mismatch: The One-Character OAuth Fix

Google's 400 redirect_uri_mismatch means the console URL and your callback differ by one character. The 5-minute exact-match checklist. Fix inside.

Clerks sorting undeliverable mail at tables in the Dead Letter Office, early 1900s
Clerks sorting undeliverable mail at tables in the Dead Letter Office, early 1900s · Photo: Bain News Service, public domain, via Wikimedia Commons

Error 400, login dead, launch blocked

You click "Sign in with Google." Instead of consent, Google shows Error 400: redirect_uri_mismatch — with the maddening detail that everything looks right. The console has a URL.

Your app has a URL. They look identical.

They differ by one character. A trailing slash. http vs https. A path no human eye catches at midnight before launch.

Google oauth redirect_uri_mismatch fix is pure exact-matching: the URL your app sends must byte-match a URL registered in the console. No fuzzy logic, no helpful suggestions, just 400 until perfect.

Match dimensions that all must agree

Dimension App sends Console must hold Common miss
Scheme https (prod) https http leftover from dev
Host app.example.com app.example.com www. vs bare
Port implicit 443 no port listed :3000 copied to prod
Path /api/auth/callback/google identical path Auth.js v5 path differs!
Trailing slash none none One side adds /
Query params none allowed none registered ?x=1 appended

Three details make this error recur across environments:

  1. Dev and prod URLs differ everywhere. Localhost :3000 over http, staging subdomain, prod apex — three registrations minimum, and the launch checklist forgets staging until the demo breaks in front of a customer. Register all three on day one.
  2. Auth.js v5 changed callback paths. Upgrades from NextAuth v4 to v5 (or Auth.js) shift route structures; the console still holds v4 paths while the app sends v5 ones. Every auth upgrade must include a console-URL audit — the version-hell post covers the siblings of this exact break.
  3. The error page shows the truth. Google's 400 page prints the exact redirect_uri your app sent — copy it verbatim instead of retyping from memory. The fix is transcription, not reasoning: take the printed string, paste it into authorized origins, wait two minutes, retry.

Console configuration, never code. Open the dashboard before the editor — every time.

Google oauth redirect_uri_mismatch fix checklist

Five minutes with both tabs open: your app's error page and Google Cloud console → APIs & Services → Credentials → OAuth client.

Step 1 — Copy the sent URL verbatim from the error.

Error 400 page shows, word for word:
  redirect_uri=https://app.example.com/api/auth/callback/google
Copy it. Do not retype it. Retyping reintroduces the invisible character.

Paste into a scratch note. This string is ground truth — the console must contain exactly it, and every other theory waits until it does.

Step 2 — Register it plus siblings in the console.

Authorized redirect URIs (all three, day one):
  http://localhost:3000/api/auth/callback/google        (dev)
  https://staging.example.com/api/auth/callback/google  (staging)
  https://app.example.com/api/auth/callback/google      (prod)
Authorized JavaScript origins: the bare origins (no path):
  http://localhost:3000  https://staging.example.com  https://app.example.com

Origins (no path) and redirect URIs (full path) are separate lists — the most common partial fix registers one and forgets the other. Save, wait ~2 minutes for propagation (console changes are not instant), retry.

Still 400? Re-copy from the fresh error page — the sent URL may have changed with your fix attempt.

Step 3 — Lock it with env parity so it never recurs.

Give each environment one canonical site address in its settings, with no guessing: the localhost address for local development, and your real https:// domain for production, synced to the server with bp secret sync and never committed. Before deploying, check that the redirect your app sends equals a registered URI, and diff them in review.

Site-address drift is the repeat offender. A staging setting pointing at prod URLs, or the other way round, manufactures mismatches no console edit can fix.

Review diffs should flag URL changes like code changes. Configuration is checked-in intent, not tribal memory.

That's the fences at work — logins that work on every domain you own.

The mismatch drill card

Check Action Pass looks like
Dev login localhost Google click Consent → session, first try
Staging login staging subdomain click Same, no console edits needed
Prod login apex domain click Same — registered day one
Upgrade audit After any auth version bump Callback paths re-verified
Error literacy Force a wrong URI once Team reads the 400 page, copies verbatim

Deliberately break staging once per quarter (wrong URI, read the 400, fix by transcription) so the skill stays warm. Mismatch literacy is a five-minute skill that blocks launches when absent — drill it like the rollback flip: rarely needed, instantly available.

Straight talk: Google's exact-match strictness is security working as designed (open-redirect prevention for the world's most phished flow), and the price is developer friction at every URL change.

BoringPush pays it gladly — the alternative (loose matching) is how accounts get stolen. Strictness you checklist once beats breaches you explain forever.

FAQ

Why does google oauth redirect_uri_mismatch fix work in dev but break in prod?

Because dev and prod are different registrations and only dev got registered. The app sends prod's URL (correct behavior), the console lacks it (incomplete setup), Google 400s (correct enforcement).

Fix: register all three environments day one, and treat "works locally" as necessary-but-insufficient evidence for auth. Environment parity is the whole game.

How long do console changes take to propagate?

Roughly seconds to a few minutes — retry after two, suspect something else after ten. The classic trap is "fixing" a correct registration repeatedly during the propagation window, stacking duplicate URIs while the real issue (trailing slash) sits untouched.

Change once, wait, re-copy the error string, then reason. Patience is a debugging tool.

Does the mismatch error expose anything sensitive?

No — the 400 page shows your own redirect URI (public by design) plus an error code. It reveals nothing about secrets, users, or configuration beyond what your login button already advertises. Screenshot it freely into team chats and issues; the string on the page is the fix recipe, not a leak.

Different mechanism, same discipline: magic-link URLs embed tokens plus callback paths that must match the app's configured site address. Staging links clicked against prod env (or vice versa) fail identically in spirit. Keep one canonical URL per environment and the whole class — OAuth and links alike — stays quiet.

Can I use one OAuth client for dev, staging, and prod?

Yes — one client holding all three redirect URIs is standard practice and reduces secret sprawl (one client ID/secret pair, env-synced per environment). Separate clients per environment add isolation at the cost of triple configuration drift.

Solo founders: one client, three URIs, documented. Teams with compliance: split when auditors say so.

Cousin, not sibling: mismatch blocks at Google (400 page), loops happen after return (session never sticks — cookie flags, secure on http, or the stale-session trap). Fixed the 400 and now looping?

Read the stale-session post next — different checklist, same five-minute shape. Auth errors queue politely; fix them in arrival order.

Keep reading

+ all the boring stuff (analytics, auth, SQLite ↔ Supabase, full marketplace, webhooks)

The Agent-First Shipping Kit

Command from your phone.

Built so autonomous AI agents build what you want without breaking what's already paying you.

$199

Get BoringPush

One-time payment · Lifetime updates · Unlimited projects.

Keep reading