Cybersecurity / Networking

OAuth 2.0 "invalid_grant" Redirect URI Mismatch Fix

An invalid_grant error during an OAuth token exchange very often means the redirect_uri sent in the token request doesn't exactly match the one used in the original authorization request — OAuth providers require this to match character-for-character as a security measure, and even a trivial difference triggers this rejection.

The Problem

After a user authorizes your app and gets redirected back with an authorization code, exchanging that code for an access token fails:

{
  "error": "invalid_grant",
  "error_description": "The provided authorization grant is invalid, expired, revoked, does not match the redirection URI used in the authorization request, or was issued to another client."
}

The error message itself hints at several possible causes, but a redirect URI mismatch is by far the most common one for a flow that otherwise looks correctly implemented.

Why It Happens

OAuth 2.0 requires the redirect_uri parameter sent during the token exchange step to exactly match the one sent during the initial authorization request — this is a deliberate security measure preventing authorization codes from being redeemed by a different, potentially malicious redirect target than the one the user actually approved. Common causes of an unintended mismatch:

  • A trailing slash present in one request but not the other (https://app.com/callback versus https://app.com/callback/) — these are treated as genuinely different strings by most OAuth providers, even though they might resolve to the same actual endpoint in a browser
  • An http versus https mismatch between the two requests, common during local development when testing against a production OAuth app configuration
  • A different port number, subdomain, or exact case in the hostname between the two requests
  • The registered redirect URI in the OAuth provider's dashboard doesn't exactly match either request, causing the mismatch to originate from the provider-side configuration rather than anything different between your two actual requests to each other

The Fix

First, log and compare the exact redirect_uri value sent in both the authorization request and the subsequent token exchange request, character for character:

console.log('Authorization redirect_uri:', authRedirectUri);
console.log('Token exchange redirect_uri:', tokenRedirectUri);

Even a single character difference (a trailing slash, different capitalization) is enough to trigger this error, so a visual comparison in code, not just a glance, is worth doing carefully.

Use a single, shared constant for the redirect URI value throughout your codebase, referenced identically in both the authorization request and the token exchange request, rather than constructing or hardcoding the URL separately in two different places where a subtle difference could accidentally be introduced:

const REDIRECT_URI = 'https://app.example.com/auth/callback';

// Use this exact constant in both places:
// 1. Building the authorization URL
// 2. Making the token exchange request

Confirm the redirect URI registered in your OAuth provider's application settings dashboard exactly matches what your code actually sends — a mismatch here specifically produces this same error, even if your two requests to each other are perfectly consistent with one another, since the provider validates against its own registered value as well.

If you're testing locally with a different URL than production (like http://localhost:3000/callback versus your production HTTPS URL), most OAuth providers allow registering multiple valid redirect URIs for the same application — register the local development URL explicitly as an additional valid redirect URI, rather than trying to reuse a production-only redirect URI value during local testing:

# Provider dashboard - typically supports multiple registered URIs:
https://app.example.com/auth/callback
http://localhost:3000/auth/callback

Ensure your environment-specific configuration correctly selects the appropriate redirect URI based on the current environment, rather than accidentally using a hardcoded production value during local development or vice versa.

Still Not Working?

If the redirect URIs are confirmed to match exactly and the error persists, check whether the authorization code itself might genuinely be expired, already used, or revoked — the same invalid_grant error covers these cases too, per the error description. Authorization codes are typically single-use and short-lived (often expiring within a minute or two), so a delay between receiving the code and exchanging it — from a slow network request, a debugging breakpoint, or a retry after an earlier failed attempt reusing the same already-consumed code — produces the identical error message despite having nothing to do with the redirect URI at all.