Error 400: redirect_uri_mismatch on localhost, fixed for local dev

Updated 26 Oct 2026 · Pewterkit Guides

On localhost, Google compares the whole redirect URI, including http://, the host spelling (localhost is not 127.0.0.1) and the port, so a dev server that moved from port 3000 to 3001 is enough to break sign-in. Pin your dev port, register that exact http://localhost:<port>/<path> URI on the OAuth client, and if you use Firebase's popup or redirect sign-in, note that the redirect URI isn't on localhost at all.

First: which kind of sign-in are you running?

The fix depends on who builds the redirect_uri:

Your setup What Google receives as redirect_uri Do you register a localhost URI?
Firebase signInWithPopup / signInWithRedirect https://<authDomain>/__/auth/handler (by default https://<project-id>.firebaseapp.com/__/auth/handler) No. See "Firebase on localhost" below.
Your own server flow, Auth.js, Passport, google-auth-library A callback on your dev server, for example http://localhost:3000/api/auth/callback/google Yes, the exact URI including port
Firebase Authentication emulator No real Google request: the emulator serves its own local sign-in page No
A CLI or desktop tool (not a web page) A loopback address such as http://127.0.0.1:<port> Use a Desktop app client instead of a Web client

Why localhost breaks so often

Google's rules for web clients, as written in its OAuth docs:

  • The value "must exactly match one of the authorized redirect URIs", and "the http or https scheme, case, and trailing slash ('/') must all match."
  • Redirect URIs must use HTTPS, but "Localhost URIs (including localhost IP address URIs) are exempt from this rule." So http://localhost:5173/... is allowed.
  • "Hosts cannot be raw IP addresses. Localhost IP addresses are exempted from this rule." So http://127.0.0.1:5173/... can be registered, but a LAN address such as http://192.168.1.20:5173/... cannot.
  • Wildcard characters (*) are not allowed, so there is no "any port" entry for a web client.

Local dev adds three ways to send a different string than the one you registered:

  1. The port moved. Vite's default port is 5173, and unless server.strictPort is true it automatically tries the next available port when 5173 is busy. Next.js defaults to 3000 and takes -p or PORT to change it.
  2. The host spelling changed. Opening the app at 127.0.0.1 instead of localhost (or the reverse) changes any callback URL built from the page's own address.
  3. The scheme changed. next dev --experimental-https serves the app at https://localhost:3000, so the callback becomes https://..., not http://....

Step-by-step fix

1. Read the redirect_uri your app actually sends

Open the browser's Network panel with Preserve log ticked, start sign-in, and read redirect_uri and client_id from the request to accounts.google.com (for a popup, look in the popup window). The full method is in Error 400: redirect_uri_mismatch. Note the exact host and port.

2. Pin the dev server's port

Vite (vite.config.js):

import { defineConfig } from "vite";

export default defineConfig({
  server: {
    port: 5173,
    strictPort: true, // exit instead of silently moving to 5174
  },
});

Next.js (package.json):

{
  "scripts": {
    "dev": "next dev -p 3000"
  }
}

Next.js notes that PORT cannot be set in .env, because the HTTP server starts before other code loads, so use -p or set PORT in the shell.

3. Use one host spelling

Pick localhost or 127.0.0.1 and always open the app with that spelling. Vite's server.host defaults to 'localhost'. If teammates use both, register both variants.

4. Register the exact URI on the OAuth client

In Google Cloud console (labels as of Oct 2026): Google Auth Platform > Clients (older menus: APIs & Services > Credentials), open the Web client whose ID matches client_id, and add under Authorized redirect URIs the value from step 1. Examples:

http://localhost:5173/auth/callback
http://localhost:3000/api/auth/callback/google
http://127.0.0.1:3000/api/auth/callback/google

The middle line is the Auth.js pattern: your site URL plus /api/auth/callback/google.

If you use Google Identity Services and also need Authorized JavaScript origins, Google's setup guide says: "For local tests or development add both http://localhost and http://localhost:<port_number>". Origins are a separate list and do not fix redirect_uri_mismatch.

5. Wait, then retest

Google says "It may take 5 minutes to a few hours for changes made to these settings to take effect." Retesting seconds after saving can still fail.

Firebase on localhost

With Firebase's signInWithPopup or signInWithRedirect, Google doesn't send its OAuth response to your dev server. It returns to the helper at https://<authDomain>/__/auth/handler, and Firebase's troubleshooting FAQ lists https://PROJECT_ID.firebaseapp.com/__/auth/handler as the web client's redirect URI. So:

  • In dev, keep authDomain: "<project-id>.firebaseapp.com". That handler URI is the one the FAQ lists for the web client.
  • Don't set authDomain to localhost:5173 on its own. Google would then be asked to return to a /__/auth/handler URL on your dev server, which is not on the client's list, and a plain dev server doesn't serve Firebase's handler there. The exception is a dev server that proxies /__/auth to Firebase over HTTPS, with that handler URI registered; that setup is in signInWithRedirect on localhost.
  • If you see auth/unauthorized-domain instead of redirect_uri_mismatch, the missing piece is in Firebase, not Google: projects created after April 28, 2025 no longer include localhost as an authorized domain by default. That fix is covered in auth/unauthorized-domain on localhost.
  • Firebase's Authorized domains entry covers "any URL and port of that domain", so the port problem above applies to Google's redirect URIs, not to Firebase's list.
// firebase.js: dev config that keeps Google's redirect on the firebaseapp.com handler
import { initializeApp } from "firebase/app";
import { getAuth } from "firebase/auth";

const app = initializeApp({
  apiKey: "<api-key>",
  authDomain: "<project-id>.firebaseapp.com", // the default handler host
  projectId: "<project-id>",
  appId: "<app-id>",
});

export const auth = getAuth(app);

Redirect sign-in on localhost has its own pitfalls beyond this error; see signInWithRedirect on localhost.

Skip Google entirely with the Auth emulator

For Firebase SDK sign-in flows, "the Authentication emulator serves a local version of the corresponding sign-in page to help you test authentication from web apps that call the signinWithPopup or signInWithRedirect method", and it "creates mock third-party user accounts and credentials as needed". No Google OAuth client is involved, so redirect_uri_mismatch can't occur:

import { getAuth, connectAuthEmulator } from "firebase/auth";

const auth = getAuth();
if (import.meta.env.DEV) {
  connectAuthEmulator(auth, "http://127.0.0.1:9099");
}

Start it with firebase emulators:start. import.meta.env.DEV is Vite's dev-mode flag; use your framework's equivalent elsewhere.

Building a CLI or desktop tool?

Google's installed-app docs describe a loopback redirect for "macOS, Linux, and Windows desktop apps": the app listens on a random free port and uses http://127.0.0.1:<port> (or http://[::1]:<port>). This applies to OAuth clients of type Desktop app. Google notes localhost can be used in place of the loopback IP, "but this configuration may cause issues with client firewalls", and that the loopback option is deprecated for Android, Chrome app and iOS client types. If you created a Web application client for a CLI, every port it picks needs its own registered URI, so switch the client type.

How to confirm it worked

  1. Restart the dev server and check its startup log shows the port you pinned.
  2. Open the app at exactly the host and port you registered.
  3. Start sign-in. You should see Google's account chooser instead of Error 400.
  4. If it still fails, compare the redirect_uri in the Network panel with the client's list again: port, localhost vs 127.0.0.1, http vs https, trailing slash.

FAQ

Do I need both http://localhost and http://127.0.0.1 redirect URIs?

Only if you open the app both ways. Google matches the host exactly, so register the spelling your browser and your callback code actually use, or both.

Can I register a redirect URI for any localhost port?

Not on a Web application client: wildcard characters aren't allowed and the port must match exactly. Pin the dev server's port instead. Desktop app clients use a loopback redirect where the app picks a free port.

Why does it work for my teammate but not for me?

Usually a different port (one dev server moved to 5174 or 3001) or a different host spelling. Compare the redirect_uri each of you sends with the client's list.

Can I test Google sign-in from my phone using my computer's LAN IP?

Not with a registered redirect URI on that IP: Google doesn't accept raw IP address hosts except localhost IPs. Use an HTTPS hostname you control, or test on the computer itself.

Does Firebase need a localhost redirect URI on the Google OAuth client?

Not for signInWithPopup or signInWithRedirect with the default authDomain: Google returns to https://<project-id>.firebaseapp.com/__/auth/handler. Localhost needs to be in Firebase's Authorized domains list instead.

Sources

  1. Using OAuth 2.0 for Web Server Applications (Google Identity): exact match, Redirect URI validation rules
  2. OAuth 2.0 for iOS & Desktop Apps (Google Identity): loopback IP address redirect
  3. Get your Google API client ID (Google Identity Services): localhost origins
  4. Manage OAuth Clients (Google Cloud Console Help): Clients page, propagation delay
  5. Server Options (Vite): server.port, server.strictPort, server.host
  6. Env Variables and Modes (Vite): import.meta.env
  7. next CLI (Next.js): dev port, PORT, --experimental-https
  8. Google provider (Auth.js): callback URL format
  9. Firebase Authentication FAQ and troubleshooting (Firebase): web client redirect URIs, localhost authorized domain change
  10. Set a web app's OAuth redirect domains (Firebase Help): any URL and port of that domain
  11. Best practices for using signInWithRedirect (Firebase): /__/auth/handler at authDomain
  12. Connect your app to the Authentication Emulator (Firebase): connectAuthEmulator, emulated IDP sign-in
  13. Network features reference (Chrome DevTools): Preserve log

How this page was made: drafted with AI from the sources listed, reviewed by Pewterkit. Docs checked 26 Oct 2026. Spotted something out of date? Tell us.

Related guides