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.

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:
- Dev and prod URLs differ everywhere. Localhost
:3000overhttp, 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. - 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.
- The error page shows the truth. Google's 400 page prints the exact
redirect_uriyour 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.comOrigins (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.
What about redirect_uri_mismatch on magic links instead of Google?
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.
After fixing, logins loop instead of erroring — related?
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
- Next.js Auth in an Afternoon: Google + Magic Links — the setup this fix unblocks
- Stale Sessions Bounce You Forever (One-Rule Fix) — the cousin error after the 400 clears
- Auth Without the 2 a.m. Pages: Google + Magic Links — the founding sketch both posts complete
+ all the boring stuff (analytics, auth, SQLite ↔ Supabase, full marketplace, webhooks)
Command from your phone.
Built so autonomous AI agents build what you want without breaking what's already paying you.
- Built-in analytics — cookie-free mode, $0 forever, no PostHog bill
- SQLite ↔ Supabase: one setting, same field names
- Full digital marketplace & feedback boards included
- 3 payment rails: Stripe, LemonSqueezy & Dodo Payments
- Agent fences: lint gate, read-only data access, auto-rollback
- Ship from your phone via SSH & WireGuard/Tailscale in under a minute
$199
Get BoringPushOne-time payment · Lifetime updates · Unlimited projects.
Keep reading


