OAuth Redirect Doctoran ARLing tool

Error 400: redirect_uri_mismatch. Find the one character that differs.

Paste the redirect_uri from Google's error page, your Authorized redirect URIs, and your stack: NextAuth, Supabase, Firebase, Passport, Django, Laravel, Expo, Flutter, Postman, or gcloud. Get the one value that is different, byte for byte.

01

Three reasons Google keeps rejecting a URI that looks right.

Google's redirect_uri check is an exact string comparison. No wildcards, no normalization. These three causes account for almost every mismatch.

  1. Exact match only.

    Scheme, host, port, path, case, and trailing slash must all match. localhost and 127.0.0.1 count as different hosts. One differing character is enough to fail.

  2. Wrong target.

    Supabase and Firebase need their own callback URL in Google Console, not your app's URL: Google redirects to them first. NextAuth and Auth.js need a specific /api/auth/callback/google path.

  3. Wrong client type.

    A Web application OAuth client only accepts http and https URIs. A custom scheme or a loopback port needs a Desktop, Android, or iOS client instead.

02

Fill in what Google and your console show. Get the exact diff.

Nothing you type is sent anywhere. The check runs in your browser, using Google's documented redirect_uri rules.

fn GoogleRedirectDoctor.diagnose(config)
client-side, no network
Google's error page
From the address bar's redirect_uri= param, the page's request details, or your Network tab on the accounts.google.com/o/oauth2/v2/auth request.
Google Cloud Console → Credentials
Your app
Only used when Stack = Supabase.
Only used when Stack = Firebase.
Result · idle
Fill in the form and press Diagnose or ⌘↵ to see the result.
status
idle
not run yet

03

One function. Config in, diagnosis out.

No server, no API key, no signup. doctor-google.js is plain JavaScript: read it, fork it, or run it in your own scripts or CI.

doctor-google.js 0 dependencies
import { diagnose } from './doctor-google.js';
// or, loaded globally: const { diagnose } = window.GoogleRedirectDoctor;

const result = diagnose({
  error:   { redirectUriFromError, clientType },
  console: { authorizedRedirectUris, authorizedJsOrigins, publishingStatus, savedMinutesAgo },
  app:     { stack, origin, callbackPath, supabaseRef, firebaseAuthDomain,
             expoUsername, expoSlug, useProxy, port, env },
});

// result
{
  "status":     "fail" | "warn" | "pass",
  "summary":    "…",
  "expected":   { stack, redirectUri, redirectUriNote, clientTypeExpected, … },
  "problems":   [ { severity, code, message, path, value, fix } ],
  "fixes":      [ { title, value, where } ],
  "checklist":  [ "…" ],
  "disclaimer": "…"
}

Everything runs client-side. The form above calls this exact function in your browser. There is no backend, no API key, and no request that carries your config anywhere.

It computes the redirect_uri your stack should send, diffs it byte for byte against what Google saw and against every entry in your Authorized redirect URIs, and reports which part differs: scheme, host, port, path, case, trailing slash, query, or fragment.

Free and open source. Found a case it gets wrong? Open a GitHub issue on the repo. No ads, and no tracking beyond anonymous usage counts.

04

Free.

No account, no payment, no usage limit: it runs as a static page in your browser, so there is no server to bill for.

05

Questions developers actually search for.

Straight answers to the same redirect_uri_mismatch questions this tool diagnoses, for when you just need the answer, not the checker.

What does Error 400: redirect_uri_mismatch mean?

Google's OAuth server got a redirect_uri that is not byte-for-byte identical to one of the Authorized redirect URIs on your OAuth 2.0 Client ID. Google's docs are explicit: scheme, host, port, case, and trailing slash must all match. There is no wildcard matching, unlike Supabase's redirect allow-list. Either your app sends the wrong URI, or the right one is not registered yet.

Which redirect URI does NextAuth or Auth.js send to Google?

By default, your site's origin plus /api/auth/callback/google, for example https://myapp.com/api/auth/callback/google. This applies to NextAuth v4 and Auth.js v5, App Router and Pages Router alike. A custom basePath replaces /api/auth with your own prefix. Register that exact string, with no trailing slash, in Google Cloud Console.

What redirect URI do I put in Google Cloud Console for Supabase?

Supabase's own callback: https://<project-ref>.supabase.co/auth/v1/callback, or your custom Supabase auth domain if you set one. Never your app's URL. Google redirects to Supabase first, and only then does Supabase redirect to your app using its own separate Redirect URLs allow-list. Pasting your app's URL here instead is one of the most common causes of this error.

Why does localhost work but 127.0.0.1 does not, or the other way round?

Google treats localhost and 127.0.0.1 as two different hosts, even though both point at your machine. Its exact-match rule does not normalize them. If your code sends one and you registered the other, you will see this error. Register the exact host string your code actually sends.

Why do I still get redirect_uri_mismatch after adding the URI?

Three usual reasons. The value you added is not byte-identical to what your app sends (a stray trailing slash, http vs https, www vs apex, or a hidden character from pasting). You edited the wrong OAuth Client ID or the wrong Google Cloud project. Or the change has not propagated yet: Google's own guidance says it can take minutes up to a few hours.

Can I use a wildcard for Vercel preview deployments?

No. Unlike Supabase's redirect allow-list, Google's Authorized redirect URIs field accepts no wildcards. Every entry is an exact, literal string, and each Vercel preview gets its own *.vercel.app URL. Route the callback through a stable domain, even from previews, or register each preview URL individually.