Back to Blog
TutorialsOctober 24, 202616 min read

redirect_uri_mismatch: Fix the OAuth 2.0 Error Step by Step

redirect_uri_mismatch means your app sent a redirect URI that is not character-for-character identical to one you registered. Here is how to find the difference and fix it for Google, GitHub and Microsoft.

redirect_uri_mismatch: Fix the OAuth 2.0 Error Step by Step

11 min read · Last updated: October 25, 2026

The redirect_uri_mismatch error means the redirect_uri your app sent in the OAuth 2.0 authorization request is not an exact match for any redirect URI registered on that client ID. Providers compare the two as plain strings, so one trailing slash, http instead of https, a different port or a different path is enough to fail.

Came here from Instagram, LinkedIn, X or YouTube? Here is the full source list you came for. Everything below is built from these, nothing else.

The specs

The Google docs

Other providers

Key takeaways

  • OAuth 2.0 tells servers to compare redirect URIs with simple string comparison (RFC 6749, Section 3.1.2.3). There is no "close enough".
  • Google says the match must be exact including the scheme, case and trailing slash.
  • Google settings changes can take 5 minutes to a few hours to take effect, so a correct fix can look broken for a while.
  • The one exception is loopback: RFC 8252 says servers MUST allow any port for loopback redirect URIs in native apps.
  • The redirect_uri in the token exchange MUST be identical to the one in the authorization request (RFC 6749, Section 4.1.3).
  • Use a random state and PKCE with S256 on every authorization code flow, as RFC 9700 recommends.

What you'll build and why

By the end you will have two things. First, a repeatable way to debug redirect_uri_mismatch in under five minutes on Google, GitHub or Microsoft. Second, a small, correct Node.js login flow with Google that sends one redirect URI from one place, checks state, and uses PKCE.

Why bother with the second part? Because most mismatch bugs come back. You fix the trailing slash, then a proxy rewrites https to http, then staging gets a new domain. A flow that builds the redirect URI from a single config value stops that loop.

I have hit this error on every OAuth integration I have shipped at Art of Code. The fix is almost always boring. Finding it is the hard part.

Prerequisites

  • Node.js 18 or later (for built-in fetch) and Express.
  • A Google Cloud project with an OAuth client of type Web application.
  • Access to the client's settings in the Google Auth Platform Clients page.
  • Basic idea of HTTP redirects. If you want a refresher on how status codes behave, read why your private API doesn't need HTTP status codes.

What is a redirect URI?

A redirect URI is the address on your app where the authorization server sends the user back after they approve or deny access, carrying an authorization code (or an error) in the query string. You register it in advance, and the server refuses to send codes anywhere else. That refusal is what redirect_uri_mismatch is.

RFC 6749 calls this the redirection endpoint. It sets two hard rules: the URI "MUST be an absolute URI" and it "MUST NOT include a fragment component" (the #... part).

Why so strict? The code in that redirect can be exchanged for tokens. If a server accepted any URI that looked similar, an attacker could register a lookalike path and collect codes. Microsoft's docs put it plainly: the server "won't redirect users or send tokens to a URI that isn't added to the app registration."

RFC 9700, the current security best practice from January 2025, goes further. Authorization servers "MUST utilize exact string matching except for port numbers" in localhost redirect URIs.

How do you fix Google OAuth redirect_uri_mismatch?

To fix redirect_uri_mismatch in Google OAuth, find the exact redirect_uri value your app sent, open your client in the Google Auth Platform Clients page, and add that exact string under Authorized redirect URIs. Match the scheme, host, port, path, case and trailing slash. Then wait, because changes can take 5 minutes to a few hours.

Here are the steps in order.

Step 1: Capture the redirect_uri your app actually sent

Do not trust your config file. Trust the request. Open the login link, and when the error page appears, copy the full URL of the authorization request from the browser (or from your network tab). Then decode it:

// node show-redirect.mjs "https://accounts.google.com/o/oauth2/v2/auth?..."
const url = new URL(process.argv[2]);
console.log(JSON.stringify(url.searchParams.get("redirect_uri")));

JSON.stringify wraps the value in quotes, so a trailing slash or a stray space is visible.

Step 2: Copy the registered value from the console

Go to the Google Auth Platform Clients page, select your project, open the OAuth client, and copy the entry under Authorized redirect URIs. Check you opened the right client ID. Having separate dev and prod clients is good practice, and it is also the most common reason people edit the wrong one.

Step 3: Compare them as strings

const sent = "http://localhost:3000/auth/google/callback/";
const registered = "http://localhost:3000/auth/google/callback";

if (sent === registered) {
  console.log("Match");
} else {
  const i = [...sent].findIndex((ch, n) => ch !== registered[n]);
  console.log(`First difference at index ${i}:`, JSON.stringify(sent.slice(i)), "vs", JSON.stringify(registered.slice(i)));
}

This is exactly how the server thinks. OpenID Connect Core says the URI "MUST exactly match one of the Redirection URI values" using simple string comparison.

Step 4: Fix the difference you found

Use this table to match what you see to a fix.

What differsExample sent vs registeredFix
Trailing slash/callback/ vs /callbackRegister the exact form your code sends, or change the code
Schemehttp://app.com vs https://app.comUse https; check your reverse proxy passes the original scheme
Portlocalhost:5173 vs localhost:3000Register each dev port you use, or pin the dev server port
Hostwww.app.com vs app.comRegister the canonical host and redirect the other one to it
Case/Auth/Callback vs /auth/callbackUse one lowercase path everywhere
Path/api/auth/callback/google vs /callbackLog the path your auth library builds, and register that
Wrong clientDev client ID with prod URICheck the client_id in the same decoded URL

On Google, also check the URI is allowed at all. Google's rules say redirect URIs must use HTTPS (localhost is exempt), cannot be a raw IP address other than localhost, cannot contain a fragment, userinfo, path traversal like /.., or wildcards (*).

Step 5: Send the same value in the token exchange

The redirect happens twice in a sense. Once in the authorization request, and once as a parameter when you swap the code for tokens. RFC 6749, Section 4.1.3, says if redirect_uri was in the authorization request, the two values "MUST be identical." If your login works but the code exchange fails, this is a strong suspect.

Step 6: Wait, then retest in a clean window

Google's help center says: "It may take 5 minutes to a few hours for changes made to these settings to take effect." Retest in a private window so old sessions do not confuse you. If it still fails after a few hours, go back to Step 1, because the sent value is almost always the real answer.

Why does redirect_uri_mismatch happen on localhost, mobile and desktop apps?

On localhost and in native apps, the mismatch usually comes from ports and URI schemes. RFC 8252 says servers must accept any port for loopback IP redirect URIs, but each provider implements that differently, and Google no longer supports custom URI schemes for Android or Chrome apps. The redirect type must match the client type you created.

Here is how the big three handle the same cases:

RuleGoogleGitHubMicrosoft Entra ID
MatchingExact, including scheme, case, trailing slashExact, unless wildcard matching is enabledExact, case-sensitive
http allowedOnly for localhostNot stated for non-loopbackOnly for localhost
Loopback portDesktop apps listen on a random portPort need not match the callback URLPort ignored for localhost
WildcardsNot allowedOptional, subdomains and subdirectoriesOnly for work or school accounts, via manifest
Error you seeredirect_uri_mismatchRedirect URI mismatchAADSTS50011

Google vs GitHub

A few details worth knowing:

  • Desktop apps: Google recommends the loopback IP address for macOS, Linux and Windows apps. You start an HTTP listener on "a random available port" and put that port in the URI.
  • Prefer 127.0.0.1: RFC 8252 says "the use of localhost is NOT RECOMMENDED" for loopback redirects, and recommends 127.0.0.1 or [::1]. GitHub and Microsoft repeat this advice. Note that Microsoft does not currently support [::1].
  • Android and Chrome apps: Google says "Custom URI schemes are no longer supported on Android and Chrome apps", and the loopback option is deprecated for Android, Chrome app and iOS client types. Use the platform guides instead.
  • iOS: Google Sign-In for iOS uses a URL scheme built from your reversed client ID, such as com.googleusercontent.apps.1234567890-abcdefg, added to CFBundleURLSchemes in Info.plist.
  • The old copy and paste flow: Google says the out-of-band (OOB) method "is no longer supported". If an old tutorial uses it, the mismatch error is the result.
  • No embedded webviews: RFC 8252 says native apps "MUST NOT use embedded user-agents" for authorization requests. Open the system browser.

One GitHub trap: apps that had a single callback URL before August 3, 2026 have wildcard matching turned on for it. GitHub warns this lets an attacker "send authorization codes to any subdomain or subdirectory". Consider turning it off.

Step 7: Build the flow so the mismatch cannot come back

This is the Node.js and Express flow from the intro. One environment variable holds the redirect URI, and both requests read it.

npm install express express-session
import express from "express";
import session from "express-session";
import crypto from "node:crypto";

const app = express();
app.use(session({ secret: process.env.SESSION_SECRET, resave: false, saveUninitialized: false }));

const REDIRECT_URI = process.env.GOOGLE_REDIRECT_URI;

app.get("/auth/google", (req, res) => {
  const state = crypto.randomBytes(32).toString("base64url");
  const verifier = crypto.randomBytes(32).toString("base64url");
  const challenge = crypto.createHash("sha256").update(verifier).digest("base64url");
  req.session.oauth = { state, verifier };

  const params = new URLSearchParams({
    client_id: process.env.GOOGLE_CLIENT_ID,
    redirect_uri: REDIRECT_URI,
    response_type: "code",
    scope: "openid email profile",
    state,
    code_challenge: challenge,
    code_challenge_method: "S256",
  });
  res.redirect(`https://accounts.google.com/o/oauth2/v2/auth?${params}`);
});

app.get("/auth/google/callback", async (req, res) => {
  const saved = req.session.oauth;
  delete req.session.oauth;
  if (!saved || req.query.state !== saved.state) {
    return res.status(400).send("Invalid state");
  }

  const tokenRes = await fetch("https://oauth2.googleapis.com/token", {
    method: "POST",
    headers: { "Content-Type": "application/x-www-form-urlencoded" },
    body: new URLSearchParams({
      code: req.query.code,
      client_id: process.env.GOOGLE_CLIENT_ID,
      client_secret: process.env.GOOGLE_CLIENT_SECRET,
      redirect_uri: REDIRECT_URI,
      grant_type: "authorization_code",
      code_verifier: saved.verifier,
    }),
  });
  const tokens = await tokenRes.json();
  res.json({ ok: tokenRes.ok, scope: tokens.scope });
});

app.listen(3000);

Set GOOGLE_REDIRECT_URI=http://localhost:3000/auth/google/callback in development and register that exact string. In production, register the https version and change only the variable.

What is the OAuth 2.0 state parameter?

The OAuth 2.0 state parameter is a random, unguessable value your app creates before the redirect, stores in the user's session, and checks when the user comes back. If the returned value does not match, someone else started that request, so you abort. RFC 6749 marks it RECOMMENDED for preventing cross-site request forgery (CSRF).

GitHub describes it as "an unguessable random string" and says if the states don't match, "you should abort the process." Google adds that because your redirect URI can be guessed, state increases your assurance the callback came from your own request.

RFC 9700 notes that PKCE also protects against CSRF, but only once you have confirmed the server supports PKCE. Doing both, as in Step 7, costs nothing.

State and the OpenID Connect nonce are different jobs. state protects the redirect. nonce, per OpenID Connect Core, associates "a Client session with an ID Token" to "mitigate replay attacks."

What is the difference between OAuth and OpenID Connect?

OAuth 2.0 is for authorization: it lets an app get an access token to call an API on a user's behalf. OpenID Connect is for authentication: the spec calls it "a simple identity layer on top of the OAuth 2.0 protocol" that adds an ID Token saying who the user is. If you want "Sign in with Google", you want OpenID Connect.

OAuth 2.0OpenID Connect
Question it answersWhat may this app access?Who is this user?
Main outputAccess tokenID Token (plus access token)
TriggerAny scopeMust include the openid scope
SpecRFC 6749OpenID Connect Core 1.0

OpenID Connect requests "MUST contain the openid scope value", which is why Step 7 asks for openid email profile.

What is the difference between JWT and OAuth?

JWT is a token format and OAuth is a protocol, so they are not alternatives. RFC 7519 defines a JWT as "a compact, URL-safe means of representing claims to be transferred between two parties." OAuth decides how tokens get issued. An OpenID Connect ID Token is always a JWT. An OAuth access token may or may not be one.

What are bearer tokens?

A bearer token is a token that works for whoever holds it. RFC 6750 defines it as a token where "any party in possession of the token" can use it, without proving possession of a cryptographic key. You send it in a header like Authorization: Bearer mF_9.B5f-4.1JqM. Treat it like a password: never log it or put it in a URL.

Where do you find Google OAuth scopes?

Google publishes every scope on its "OAuth 2.0 Scopes for Google APIs" page, grouped by API. For sign-in you need openid, email and profile. For API access, pick the narrowest scope, such as https://www.googleapis.com/auth/gmail.readonly or https://www.googleapis.com/auth/calendar.readonly. RFC 6749 says scopes are space-delimited, case-sensitive strings.

ScopeWhat Google says it allows
openidAssociate you with your personal info on Google
emailSee your primary Google Account email address
profileSee your personal info, including info you've made public
.../auth/gmail.readonlyView your email messages and settings
.../auth/calendar.readonlySee and download any calendar you can access
.../auth/driveSee, edit, create and delete all your Google Drive files

Two useful Google parameters: access_type=offline returns a refresh token on the first exchange, and include_granted_scopes=true lets you add scopes later through incremental authorization. If you are building on other Google developer APIs, our look at the Google Developer Knowledge API covers a related setup.

Common mistakes

  • Editing the wrong OAuth client. Check the client_id in the decoded URL from Step 1 against the console.
  • Building the redirect URI from the request. Behind a proxy or load balancer, your app may see http and build an http:// URI. Read it from config instead.
  • Registering both slash and no-slash forms "just in case". It hides the bug and widens what an attacker can target. Pick one.
  • Using wildcards. Google forbids them. Microsoft strongly recommends absolute URIs. Use state to remember where the user came from instead.
  • Putting URLs or secrets in state. Microsoft advises storing a key in state that points to data in browser storage, not the data itself.
  • Using the implicit flow. RFC 9700 says clients "SHOULD NOT use the implicit grant". Use authorization code with PKCE.
  • Testing a fix after 30 seconds. Google can take up to a few hours. Wait before you change three more things.

OAuth bugs are security bugs wearing a config costume. Our cybersecurity basics for developers covers the wider set of mistakes in the same family.

Next steps

  1. Move every redirect URI into one environment variable per environment.
  2. Remove unused or duplicate redirect URIs from your Google, GitHub and Microsoft clients.
  3. Add PKCE with S256 to every authorization code flow. oauth.net's guidance is "Use PKCE on every Authorization Code flow."
  4. Switch dev loopback URIs to 127.0.0.1 where the provider allows it.
  5. Ask only for the scopes you use today. Add more later with incremental authorization.

FAQ

Why do I still get redirect_uri_mismatch after adding the URI?

Three usual causes: Google has not applied the change yet (it can take 5 minutes to a few hours), you edited a different client ID than the one in the request, or the sent value still differs by a slash, scheme or port. Decode the sent redirect_uri from the request URL and compare it again.

Does a trailing slash really matter in a redirect URI?

Yes. Google states the value must match exactly, including the trailing slash, and the specs require simple string comparison. https://app.com/callback and https://app.com/callback/ are two different redirect URIs.

Can I use http for an OAuth redirect URI?

Only for localhost during development. Google and Microsoft both require https for everything else. RFC 8252 explains why loopback is the exception: the redirect never leaves the device.

Do I need state if I already use PKCE?

PKCE can protect against CSRF, but RFC 9700 says you can rely on it only after confirming the authorization server supports PKCE. A random state checked on callback is cheap, and most providers document it, so use both.

Decode the redirect_uri from your next failed request and compare it character by character before you change anything else. Then browse the free tools on Art of Code, and if you are getting ready for backend interviews, read why most developers fail system design interviews.

Written by Adithya Guttha, Founder of Art of Code.

#oauth#google oauth#authentication#openid connect#web security

Sign in to like and save posts.

Curious Adithya

Written by

Curious Adithya

@curious_adithya · 5 followers

Discussion

Comments (0)

Want to join the conversation?

Sign in to comment

No comments yet. Be the first to comment!