Error 400: redirect_uri_mismatch on localhost, fixed for local dev
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
httporhttpsscheme, 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 ashttp://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:
- The port moved. Vite's default port is 5173, and unless
server.strictPortistrueit automatically tries the next available port when 5173 is busy. Next.js defaults to 3000 and takes-porPORTto change it. - The host spelling changed. Opening the app at
127.0.0.1instead oflocalhost(or the reverse) changes any callback URL built from the page's own address. - The scheme changed.
next dev --experimental-httpsserves the app athttps://localhost:3000, so the callback becomeshttps://..., nothttp://....
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
authDomaintolocalhost:5173on its own. Google would then be asked to return to a/__/auth/handlerURL 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/__/authto Firebase over HTTPS, with that handler URI registered; that setup is in signInWithRedirect on localhost. - If you see
auth/unauthorized-domaininstead ofredirect_uri_mismatch, the missing piece is in Firebase, not Google: projects created after April 28, 2025 no longer includelocalhostas 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
- Restart the dev server and check its startup log shows the port you pinned.
- Open the app at exactly the host and port you registered.
- Start sign-in. You should see Google's account chooser instead of Error 400.
- If it still fails, compare the
redirect_uriin the Network panel with the client's list again: port,localhostvs127.0.0.1,httpvshttps, trailing slash.
Related problems
- The same error on your live site: Error 400: redirect_uri_mismatch.
- Firebase rejects localhost itself: auth/unauthorized-domain on localhost.
- Redirect sign-in returns to localhost with no user: signInWithRedirect on localhost.
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
- Using OAuth 2.0 for Web Server Applications (Google Identity): exact match, Redirect URI validation rules
- OAuth 2.0 for iOS & Desktop Apps (Google Identity): loopback IP address redirect
- Get your Google API client ID (Google Identity Services): localhost origins
- Manage OAuth Clients (Google Cloud Console Help): Clients page, propagation delay
- Server Options (Vite): server.port, server.strictPort, server.host
- Env Variables and Modes (Vite): import.meta.env
- next CLI (Next.js): dev port, PORT, --experimental-https
- Google provider (Auth.js): callback URL format
- Firebase Authentication FAQ and troubleshooting (Firebase): web client redirect URIs, localhost authorized domain change
- Set a web app's OAuth redirect domains (Firebase Help): any URL and port of that domain
- Best practices for using signInWithRedirect (Firebase): /__/auth/handler at authDomain
- Connect your app to the Authentication Emulator (Firebase): connectAuthEmulator, emulated IDP sign-in
- 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.