search
Tutorials star Featured

How to Fix: Google OAuth redirect_uri_mismatch Error - Full Tutorial

Complete guide to fix Google OAuth redirect_uri_mismatch errors. Learn how to resolve OAuth redirect URI issues with practical solutions, configuration fixes, and best practices for secure authentication.

person By Gautam Sharma
calendar_today January 8, 2026
schedule 27 min read
OAuth Google Authentication Error Security Frontend Backend API

The ‘Google OAuth redirect_uri_mismatch’ error is a common authentication issue that occurs when the redirect URI in your Google OAuth request doesn’t match the authorized redirect URIs configured in your Google Cloud Console. This error typically happens when there are discrepancies between the redirect URI used in your application and the ones registered in your Google OAuth application settings. The error prevents users from completing the OAuth flow and authenticating with Google.

This comprehensive guide explains what causes this error, why it happens, and provides multiple solutions to fix it in your applications with clean code examples and directory structure.


What is the Google OAuth redirect_uri_mismatch Error?

The “redirect_uri_mismatch” error occurs when:

  • The redirect URI in the OAuth request doesn’t match authorized URIs
  • There are trailing slashes or missing protocols in the URI
  • The domain or port doesn’t match the registered URI
  • Multiple redirect URIs are configured but the wrong one is used
  • The URI contains special characters that weren’t properly encoded
  • The application environment (dev/prod) uses different URIs
  • The URI scheme (http vs https) doesn’t match
  • Wildcard patterns don’t match the actual URI

Common Error Manifestations:

  • redirect_uri_mismatch error message from Google
  • Error 400: redirect_uri_mismatch in browser
  • OAuth flow interruption with error page
  • invalid_request with redirect_uri mismatch
  • Authentication flow failure at callback stage
  • Google OAuth consent screen not appearing
  • Redirect loop with error messages
  • Development vs production URI mismatches

Understanding the Problem

This error typically occurs due to:

  • Misconfigured redirect URIs in Google Cloud Console
  • Development vs production environment differences
  • Trailing slashes or missing protocols
  • Port number mismatches
  • Domain name variations
  • URI encoding issues
  • Localhost vs deployed environment differences
  • HTTP vs HTTPS protocol mismatches

Why This Error Happens:

Google OAuth requires that the redirect URI in the authorization request exactly matches one of the authorized redirect URIs configured in your Google Cloud Console project. This security measure prevents malicious applications from intercepting authorization codes by specifying unauthorized redirect URIs.


Solution 1: Proper Google Cloud Console Configuration

The first step is to ensure your Google Cloud Console is properly configured.

❌ Without Proper Configuration:

// ❌ OAuth request with incorrect redirect URI
const CLIENT_ID = 'your-client-id';
const REDIRECT_URI = 'http://localhost:3000/callback'; // ❌ Not registered in Google Console

const authUrl = `https://accounts.google.com/o/oauth2/v2/auth?` +
  `client_id=${CLIENT_ID}&` +
  `redirect_uri=${REDIRECT_URI}&` +
  `response_type=code&` +
  `scope=email profile`;

✅ With Proper Configuration:

Google Cloud Console Setup:

// ✅ Correct OAuth request with properly configured redirect URI
const CLIENT_ID = 'your-client-id';
const REDIRECT_URI = 'http://localhost:3000/auth/google/callback'; // ✅ Registered in Google Console

const authUrl = `https://accounts.google.com/o/oauth2/v2/auth?` +
  `client_id=${CLIENT_ID}&` +
  `redirect_uri=${encodeURIComponent(REDIRECT_URI)}&` + // ✅ Properly encoded
  `response_type=code&` +
  `scope=email profile&` +
  `access_type=offline&` +
  `prompt=consent`;

// ✅ Redirect to Google OAuth
window.location.href = authUrl;

Configuration Checklist:

// ✅ Google OAuth configuration checklist
const googleOAuthConfig = {
  // ✅ Client ID from Google Cloud Console
  clientId: process.env.REACT_APP_GOOGLE_CLIENT_ID,
  
  // ✅ Redirect URI - must match exactly what's registered
  redirectUri: process.env.NODE_ENV === 'production' 
    ? 'https://yourdomain.com/auth/google/callback'
    : 'http://localhost:3000/auth/google/callback',
  
  // ✅ Scopes required
  scopes: [
    'https://www.googleapis.com/auth/userinfo.email',
    'https://www.googleapis.com/auth/userinfo.profile'
  ],
  
  // ✅ Response type
  responseType: 'code'
};

// ✅ Build authorization URL
function buildAuthUrl(config) {
  const params = new URLSearchParams({
    client_id: config.clientId,
    redirect_uri: config.redirectUri,
    response_type: config.responseType,
    scope: config.scopes.join(' '),
    access_type: 'offline',
    prompt: 'consent'
  });

  return `https://accounts.google.com/o/oauth2/v2/auth?${params.toString()}`;
}

// ✅ Usage
const authUrl = buildAuthUrl(googleOAuthConfig);

Solution 2: Environment-Specific Configuration

❌ Without Environment Handling:

// ❌ Same redirect URI for all environments
const REDIRECT_URI = 'http://localhost:3000/callback'; // ❌ Won't work in production

// ❌ This will fail in production
window.location.href = `https://accounts.google.com/o/oauth2/v2/auth?` +
  `client_id=${CLIENT_ID}&` +
  `redirect_uri=${REDIRECT_URI}&` +
  `response_type=code&` +
  `scope=email profile`;

✅ With Environment Handling:

Environment-Based Configuration:

// ✅ Environment-specific redirect URI configuration
class GoogleOAuthConfig {
  constructor() {
    this.environments = {
      development: {
        redirectUri: 'http://localhost:3000/auth/google/callback',
        authDomain: 'http://localhost:3000'
      },
      staging: {
        redirectUri: 'https://staging.yourdomain.com/auth/google/callback',
        authDomain: 'https://staging.yourdomain.com'
      },
      production: {
        redirectUri: 'https://yourdomain.com/auth/google/callback',
        authDomain: 'https://yourdomain.com'
      }
    };
    
    this.currentEnv = process.env.NODE_ENV || 'development';
    this.config = this.environments[this.currentEnv];
  }

  // ✅ Get appropriate redirect URI for current environment
  getRedirectUri() {
    return this.config.redirectUri;
  }

  // ✅ Get authorization URL for current environment
  getAuthUrl(clientId) {
    const params = new URLSearchParams({
      client_id: clientId,
      redirect_uri: this.getRedirectUri(),
      response_type: 'code',
      scope: 'email profile',
      access_type: 'offline',
      prompt: 'consent'
    });

    return `https://accounts.google.com/o/oauth2/v2/auth?${params.toString()}`;
  }

  // ✅ Validate if redirect URI is properly configured
  validateRedirectUri() {
    const registeredUris = this.getRegisteredRedirectUris();
    const currentUri = this.getRedirectUri();
    
    const isValid = registeredUris.some(uri => uri === currentUri);
    
    if (!isValid) {
      console.warn(`Redirect URI "${currentUri}" is not registered in Google Console`);
      console.warn('Registered URIs:', registeredUris);
    }
    
    return isValid;
  }

  // ✅ Get registered redirect URIs (would come from your configuration)
  getRegisteredRedirectUris() {
    // ✅ In real implementation, this would come from your config
    // ✅ For now, return the environment-specific URI
    return [this.getRedirectUri()];
  }
}

// ✅ Usage
const oauthConfig = new GoogleOAuthConfig();

// ✅ Validate configuration before use
if (oauthConfig.validateRedirectUri()) {
  const authUrl = oauthConfig.getAuthUrl(process.env.REACT_APP_GOOGLE_CLIENT_ID);
  window.location.href = authUrl;
} else {
  console.error('OAuth configuration is invalid');
}

Multiple Environment Support:

// ✅ Advanced environment configuration with validation
class AdvancedOAuthConfig {
  constructor() {
    this.configs = {
      development: {
        clientId: process.env.REACT_APP_GOOGLE_CLIENT_ID_DEV,
        redirectUris: [
          'http://localhost:3000/auth/google/callback',
          'http://127.0.0.1:3000/auth/google/callback'
        ],
        scopes: ['email', 'profile']
      },
      production: {
        clientId: process.env.REACT_APP_GOOGLE_CLIENT_ID_PROD,
        redirectUris: [
          'https://yourdomain.com/auth/google/callback',
          'https://www.yourdomain.com/auth/google/callback'
        ],
        scopes: ['email', 'profile']
      }
    };

    this.currentEnv = process.env.NODE_ENV || 'development';
    this.currentConfig = this.configs[this.currentEnv];
  }

  // ✅ Get matching redirect URI for current host
  getMatchingRedirectUri() {
    const currentHost = window.location.origin;
    
    // ✅ Find the redirect URI that matches current host
    const matchingUri = this.currentConfig.redirectUris.find(uri => {
      const uriHost = new URL(uri).origin;
      return uriHost === currentHost;
    });

    if (!matchingUri) {
      console.warn('No matching redirect URI found for current host:', currentHost);
      console.warn('Available URIs:', this.currentConfig.redirectUris);
      // ✅ Return first available URI as fallback
      return this.currentConfig.redirectUris[0];
    }

    return matchingUri;
  }

  // ✅ Build authorization URL with matching URI
  buildAuthUrl() {
    const redirectUri = this.getMatchingRedirectUri();
    
    const params = new URLSearchParams({
      client_id: this.currentConfig.clientId,
      redirect_uri: redirectUri,
      response_type: 'code',
      scope: this.currentConfig.scopes.join(' '),
      access_type: 'offline',
      prompt: 'select_account'
    });

    return `https://accounts.google.com/o/oauth2/v2/auth?${params.toString()}`;
  }

  // ✅ Validate configuration
  validateConfig() {
    const errors = [];

    if (!this.currentConfig.clientId) {
      errors.push('Client ID is not configured');
    }

    if (!this.currentConfig.redirectUris || this.currentConfig.redirectUris.length === 0) {
      errors.push('No redirect URIs configured');
    }

    // ✅ Check if current host matches any configured URI
    const currentHost = window.location.origin;
    const hasMatchingUri = this.currentConfig.redirectUris.some(uri => {
      const uriHost = new URL(uri).origin;
      return uriHost === currentHost;
    });

    if (!hasMatchingUri) {
      errors.push(`No redirect URI matches current host: ${currentHost}`);
    }

    return {
      isValid: errors.length === 0,
      errors
    };
  }
}

// ✅ Usage with validation
const advancedConfig = new AdvancedOAuthConfig();
const validation = advancedConfig.validateConfig();

if (validation.isValid) {
  const authUrl = advancedConfig.buildAuthUrl();
  // Proceed with OAuth flow
} else {
  console.error('OAuth configuration errors:', validation.errors);
}

Solution 3: Frontend Implementation

❌ Without Proper Frontend Handling:

// ❌ Basic Google OAuth without proper redirect URI handling
function initiateGoogleAuth() {
  const clientId = 'your-client-id';
  const redirectUri = 'http://localhost:3000/callback'; // ❌ Hardcoded and potentially wrong
  
  window.location.href = `https://accounts.google.com/o/oauth2/v2/auth?` +
    `client_id=${clientId}&` +
    `redirect_uri=${redirectUri}&` +
    `response_type=code&` +
    `scope=email profile`;
}

✅ With Proper Frontend Handling:

React Hook for Google OAuth:

// ✅ React hook for Google OAuth with proper redirect URI handling
import { useState, useEffect } from 'react';

export const useGoogleOAuth = () => {
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);

  // ✅ Get redirect URI based on current environment
  const getRedirectUri = () => {
    const hostname = window.location.hostname;
    
    if (hostname === 'localhost' || hostname === '127.0.0.1') {
      return 'http://localhost:3000/auth/google/callback';
    } else if (hostname.includes('staging')) {
      return 'https://staging.yourdomain.com/auth/google/callback';
    } else {
      return 'https://yourdomain.com/auth/google/callback';
    }
  };

  // ✅ Initiate Google OAuth flow
  const initiateAuth = () => {
    setLoading(true);
    setError(null);

    try {
      const clientId = process.env.REACT_APP_GOOGLE_CLIENT_ID;
      const redirectUri = getRedirectUri();
      
      // ✅ Validate redirect URI is properly configured
      if (!clientId) {
        throw new Error('Google Client ID is not configured');
      }

      // ✅ Build authorization URL
      const authUrl = new URL('https://accounts.google.com/o/oauth2/v2/auth');
      authUrl.searchParams.append('client_id', clientId);
      authUrl.searchParams.append('redirect_uri', redirectUri);
      authUrl.searchParams.append('response_type', 'code');
      authUrl.searchParams.append('scope', 'email profile');
      authUrl.searchParams.append('access_type', 'offline');
      authUrl.searchParams.append('prompt', 'consent');

      // ✅ Redirect to Google OAuth
      window.location.href = authUrl.toString();
    } catch (err) {
      setError(err.message);
      console.error('Google OAuth initiation failed:', err);
    } finally {
      setLoading(false);
    }
  };

  // ✅ Handle OAuth callback (typically in a separate component)
  const handleCallback = async (code) => {
    if (!code) {
      setError('No authorization code received');
      return;
    }

    try {
      // ✅ Exchange authorization code for tokens
      const tokenResponse = await fetch('/api/auth/google/exchange', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({
          code,
          redirectUri: getRedirectUri()
        })
      });

      if (!tokenResponse.ok) {
        throw new Error('Failed to exchange authorization code');
      }

      const tokens = await tokenResponse.json();
      // ✅ Handle successful authentication
      console.log('OAuth successful:', tokens);
    } catch (err) {
      setError(err.message);
      console.error('OAuth callback failed:', err);
    }
  };

  return {
    initiateAuth,
    handleCallback,
    loading,
    error
  };
};

// ✅ Usage in component
import React from 'react';
import { useGoogleOAuth } from './hooks/useGoogleOAuth';

const GoogleAuthButton = () => {
  const { initiateAuth, loading, error } = useGoogleOAuth();

  return (
    <div>
      <button 
        onClick={initiateAuth} 
        disabled={loading}
        className="google-auth-btn"
      >
        {loading ? 'Signing in...' : 'Sign in with Google'}
      </button>
      
      {error && (
        <div className="error-message">
          Error: {error}
        </div>
      )}
    </div>
  );
};

Frontend OAuth Service:

// ✅ Frontend OAuth service with comprehensive error handling
class FrontendGoogleOAuthService {
  constructor() {
    this.clientId = process.env.REACT_APP_GOOGLE_CLIENT_ID;
    this.backendUrl = process.env.REACT_APP_BACKEND_URL || '';
  }

  // ✅ Get environment-appropriate redirect URI
  getRedirectUri() {
    const hostname = window.location.hostname;
    const protocol = window.location.protocol;
    
    // ✅ Handle different environments
    if (hostname === 'localhost' || hostname === '127.0.0.1') {
      return `${protocol}//${hostname}:${window.location.port}/auth/google/callback`;
    } else if (hostname.includes('staging')) {
      return `https://${hostname}/auth/google/callback`;
    } else {
      return `https://${hostname}/auth/google/callback`;
    }
  }

  // ✅ Validate redirect URI configuration
  validateRedirectUri() {
    const redirectUri = this.getRedirectUri();
    
    // ✅ Check if URI is properly formatted
    try {
      new URL(redirectUri);
      return true;
    } catch (error) {
      console.error('Invalid redirect URI:', redirectUri);
      return false;
    }
  }

  // ✅ Initiate Google OAuth flow
  async initiateAuth() {
    if (!this.clientId) {
      throw new Error('Google Client ID is not configured');
    }

    if (!this.validateRedirectUri()) {
      throw new Error('Redirect URI is not properly configured');
    }

    const redirectUri = this.getRedirectUri();
    
    // ✅ Build authorization URL
    const authUrl = new URL('https://accounts.google.com/o/oauth2/v2/auth');
    authUrl.searchParams.append('client_id', this.clientId);
    authUrl.searchParams.append('redirect_uri', redirectUri);
    authUrl.searchParams.append('response_type', 'code');
    authUrl.searchParams.append('scope', 'email profile');
    authUrl.searchParams.append('access_type', 'offline');
    authUrl.searchParams.append('prompt', 'select_account');

    // ✅ Store state for security (recommended)
    const state = this.generateState();
    sessionStorage.setItem('oauth_state', state);
    authUrl.searchParams.append('state', state);

    // ✅ Redirect to Google OAuth
    window.location.href = authUrl.toString();
  }

  // ✅ Generate state parameter for security
  generateState() {
    return Math.random().toString(36).substring(2, 15) + 
           Math.random().toString(36).substring(2, 15);
  }

  // ✅ Validate state parameter
  validateState(receivedState) {
    const storedState = sessionStorage.getItem('oauth_state');
    sessionStorage.removeItem('oauth_state'); // ✅ Clear after use
    return storedState && storedState === receivedState;
  }

  // ✅ Handle OAuth callback
  async handleCallback(code, state) {
    // ✅ Validate state parameter
    if (!this.validateState(state)) {
      throw new Error('Invalid state parameter - possible CSRF attack');
    }

    if (!code) {
      throw new Error('No authorization code received');
    }

    try {
      // ✅ Exchange authorization code for tokens
      const response = await fetch(`${this.backendUrl}/api/auth/google/exchange`, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({
          code,
          redirectUri: this.getRedirectUri()
        })
      });

      if (!response.ok) {
        const errorData = await response.json().catch(() => ({}));
        throw new Error(errorData.message || 'Failed to exchange authorization code');
      }

      const tokens = await response.json();
      
      // ✅ Store tokens securely
      this.storeTokens(tokens);
      
      return tokens;
    } catch (error) {
      console.error('OAuth callback failed:', error);
      throw error;
    }
  }

  // ✅ Store tokens securely
  storeTokens(tokens) {
    // ✅ Store access token
    if (tokens.accessToken) {
      localStorage.setItem('google_access_token', tokens.accessToken);
    }
    
    // ✅ Store refresh token (if provided)
    if (tokens.refreshToken) {
      localStorage.setItem('google_refresh_token', tokens.refreshToken);
    }
    
    // ✅ Store expiration time
    if (tokens.expiresIn) {
      const expirationTime = Date.now() + (tokens.expiresIn * 1000);
      localStorage.setItem('google_token_expires', expirationTime.toString());
    }
  }

  // ✅ Check if tokens are still valid
  areTokensValid() {
    const expirationTime = localStorage.getItem('google_token_expires');
    if (!expirationTime) return false;
    
    return Date.now() < parseInt(expirationTime);
  }

  // ✅ Get stored access token
  getAccessToken() {
    if (!this.areTokensValid()) {
      return null;
    }
    return localStorage.getItem('google_access_token');
  }

  // ✅ Clear stored tokens
  clearTokens() {
    localStorage.removeItem('google_access_token');
    localStorage.removeItem('google_refresh_token');
    localStorage.removeItem('google_token_expires');
  }
}

// ✅ Initialize service
const googleOAuthService = new FrontendGoogleOAuthService();

// ✅ Export for use in other modules
window.GoogleOAuthService = googleOAuthService;

Solution 4: Backend Implementation

❌ Without Proper Backend Configuration:

// ❌ Basic OAuth callback without proper redirect URI validation
app.get('/auth/google/callback', async (req, res) => {
  const { code } = req.query;
  
  // ❌ No validation of redirect URI
  const tokenResponse = await fetch('https://oauth2.googleapis.com/token', {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body: `code=${code}&client_id=${CLIENT_ID}&client_secret=${CLIENT_SECRET}&redirect_uri=http://localhost:3000/callback&grant_type=authorization_code`
  });
  
  // ❌ This will fail if redirect_uri doesn't match
});

✅ With Proper Backend Configuration:

Express.js OAuth Backend:

// ✅ Comprehensive Google OAuth backend implementation
const express = require('express');
const axios = require('axios');
const qs = require('qs');

const app = express();
app.use(express.json());

// ✅ Google OAuth configuration
const GOOGLE_OAUTH_CONFIG = {
  clientId: process.env.GOOGLE_CLIENT_ID,
  clientSecret: process.env.GOOGLE_CLIENT_SECRET,
  redirectUris: {
    development: [
      'http://localhost:3000/auth/google/callback',
      'http://127.0.0.1:3000/auth/google/callback'
    ],
    production: [
      'https://yourdomain.com/auth/google/callback',
      'https://www.yourdomain.com/auth/google/callback'
    ]
  }
};

// ✅ Get appropriate redirect URI based on environment
function getValidRedirectUris() {
  const env = process.env.NODE_ENV || 'development';
  return GOOGLE_OAUTH_CONFIG.redirectUris[env] || GOOGLE_OAUTH_CONFIG.redirectUris.development;
}

// ✅ Validate redirect URI
function validateRedirectUri(redirectUri) {
  const validUris = getValidRedirectUris();
  return validUris.includes(redirectUri);
}

// ✅ Exchange authorization code for tokens
async function exchangeCodeForTokens(code, redirectUri) {
  if (!validateRedirectUri(redirectUri)) {
    throw new Error(`Invalid redirect URI: ${redirectUri}`);
  }

  const tokenUrl = 'https://oauth2.googleapis.com/token';
  
  const params = {
    code,
    client_id: GOOGLE_OAUTH_CONFIG.clientId,
    client_secret: GOOGLE_OAUTH_CONFIG.clientSecret,
    redirect_uri: redirectUri,
    grant_type: 'authorization_code'
  };

  try {
    const response = await axios.post(tokenUrl, qs.stringify(params), {
      headers: {
        'Content-Type': 'application/x-www-form-urlencoded'
      }
    });

    return response.data;
  } catch (error) {
    console.error('Token exchange failed:', error.response?.data || error.message);
    throw new Error('Failed to exchange authorization code for tokens');
  }
}

// ✅ Get user info from Google
async function getUserInfo(accessToken) {
  try {
    const response = await axios.get('https://www.googleapis.com/oauth2/v2/userinfo', {
      headers: {
        'Authorization': `Bearer ${accessToken}`
      }
    });

    return response.data;
  } catch (error) {
    console.error('Failed to get user info:', error.response?.data || error.message);
    throw new Error('Failed to get user information');
  }
}

// ✅ Google OAuth callback endpoint
app.get('/auth/google/callback', async (req, res) => {
  const { code, state, error } = req.query;

  try {
    // ✅ Check for OAuth errors
    if (error) {
      console.error('Google OAuth error:', error);
      return res.status(400).json({ 
        error: 'OAuth error', 
        message: error 
      });
    }

    // ✅ Validate required parameters
    if (!code) {
      return res.status(400).json({ 
        error: 'Missing authorization code' 
      });
    }

    // ✅ Get redirect URI from session or pass it as a parameter
    // ✅ In a real app, you'd store the redirect URI in session
    const redirectUri = req.session?.oauthRedirectUri || 
                       req.query.redirect_uri || 
                       getValidRedirectUris()[0];

    if (!redirectUri) {
      return res.status(400).json({ 
        error: 'Missing redirect URI' 
      });
    }

    // ✅ Validate redirect URI
    if (!validateRedirectUri(redirectUri)) {
      console.error('Invalid redirect URI:', redirectUri);
      return res.status(400).json({ 
        error: 'Invalid redirect URI',
        message: 'Redirect URI does not match registered URIs'
      });
    }

    // ✅ Exchange code for tokens
    const tokens = await exchangeCodeForTokens(code, redirectUri);

    // ✅ Get user information
    const userInfo = await getUserInfo(tokens.access_token);

    // ✅ Create user session or JWT token
    // ✅ In a real app, you'd create a session or JWT
    const sessionToken = createSession(userInfo, tokens);

    // ✅ Redirect to frontend with session token
    res.redirect(`http://localhost:3000/auth/success?token=${sessionToken}`);
  } catch (error) {
    console.error('OAuth callback error:', error);
    res.status(500).json({ 
      error: 'Authentication failed',
      message: error.message 
    });
  }
});

// ✅ Endpoint to exchange authorization code (alternative approach)
app.post('/api/auth/google/exchange', async (req, res) => {
  const { code, redirectUri } = req.body;

  try {
    // ✅ Validate required parameters
    if (!code) {
      return res.status(400).json({ error: 'Authorization code is required' });
    }

    if (!redirectUri) {
      return res.status(400).json({ error: 'Redirect URI is required' });
    }

    // ✅ Validate redirect URI
    if (!validateRedirectUri(redirectUri)) {
      return res.status(400).json({ 
        error: 'Invalid redirect URI',
        message: 'The redirect URI does not match registered URIs'
      });
    }

    // ✅ Exchange code for tokens
    const tokens = await exchangeCodeForTokens(code, redirectUri);

    // ✅ Get user info
    const userInfo = await getUserInfo(tokens.access_token);

    // ✅ Create session
    const sessionToken = createSession(userInfo, tokens);

    res.json({
      success: true,
      token: sessionToken,
      user: {
        id: userInfo.id,
        email: userInfo.email,
        name: userInfo.name,
        picture: userInfo.picture
      },
      tokens: {
        accessToken: tokens.access_token,
        refreshToken: tokens.refresh_token,
        expiresIn: tokens.expires_in
      }
    });
  } catch (error) {
    console.error('Token exchange error:', error);
    res.status(400).json({ 
      error: 'Token exchange failed',
      message: error.message 
    });
  }
});

// ✅ Helper function to create session (simplified)
function createSession(userInfo, tokens) {
  // ✅ In a real app, you'd create a proper session or JWT
  // ✅ This is a simplified example
  const sessionData = {
    userId: userInfo.id,
    email: userInfo.email,
    name: userInfo.name,
    expiresAt: Date.now() + (tokens.expires_in * 1000)
  };
  
  // ✅ Return a simple token (in real app, use JWT or session store)
  return Buffer.from(JSON.stringify(sessionData)).toString('base64');
}

// ✅ Start server
const PORT = process.env.PORT || 3001;
app.listen(PORT, () => {
  console.log(`Server running on port ${PORT}`);
});

Advanced OAuth Configuration:

// ✅ Advanced OAuth configuration with multiple validation layers
class AdvancedGoogleOAuthService {
  constructor() {
    this.config = {
      clientId: process.env.GOOGLE_CLIENT_ID,
      clientSecret: process.env.GOOGLE_CLIENT_SECRET,
      environments: {
        development: {
          redirectUris: [
            'http://localhost:3000/auth/google/callback',
            'http://127.0.0.1:3000/auth/google/callback',
            'http://localhost:3001/auth/google/callback'
          ],
          origins: ['http://localhost:3000', 'http://127.0.0.1:3000']
        },
        production: {
          redirectUris: [
            'https://yourdomain.com/auth/google/callback',
            'https://www.yourdomain.com/auth/google/callback'
          ],
          origins: ['https://yourdomain.com', 'https://www.yourdomain.com']
        }
      }
    };
    
    this.currentEnv = process.env.NODE_ENV || 'development';
    this.environmentConfig = this.config.environments[this.currentEnv];
  }

  // ✅ Validate redirect URI with multiple checks
  validateRedirectUri(redirectUri, requestOrigin = null) {
    // ✅ Check if URI is in allowed list
    const isValidUri = this.environmentConfig.redirectUris.includes(redirectUri);
    
    if (!isValidUri) {
      console.error(`Redirect URI not in allowed list: ${redirectUri}`);
      console.error('Allowed URIs:', this.environmentConfig.redirectUris);
      return false;
    }

    // ✅ Additional security check: verify origin matches
    if (requestOrigin) {
      try {
        const uriOrigin = new URL(redirectUri).origin;
        const isValidOrigin = this.environmentConfig.origins.includes(uriOrigin);
        
        if (!isValidOrigin) {
          console.error(`URI origin not in allowed origins: ${uriOrigin}`);
          console.error('Allowed origins:', this.environmentConfig.origins);
          return false;
        }
      } catch (error) {
        console.error('Invalid redirect URI format:', redirectUri);
        return false;
      }
    }

    return true;
  }

  // ✅ Get all valid redirect URIs
  getAllowedRedirectUris() {
    return this.environmentConfig.redirectUris;
  }

  // ✅ Validate and normalize redirect URI
  normalizeRedirectUri(redirectUri) {
    try {
      // ✅ Parse and reconstruct to ensure proper formatting
      const url = new URL(redirectUri);
      
      // ✅ Ensure proper protocol
      if (this.currentEnv === 'production' && url.protocol !== 'https:') {
        throw new Error('Production environment requires HTTPS');
      }
      
      // ✅ Remove any trailing slashes
      const normalized = url.toString().replace(/\/$/, '');
      return normalized;
    } catch (error) {
      console.error('Failed to normalize redirect URI:', error);
      return null;
    }
  }

  // ✅ Exchange authorization code with validation
  async exchangeCode(code, redirectUri) {
    // ✅ Normalize and validate redirect URI
    const normalizedUri = this.normalizeRedirectUri(redirectUri);
    if (!normalizedUri) {
      throw new Error('Invalid redirect URI format');
    }

    if (!this.validateRedirectUri(normalizedUri)) {
      throw new Error(`Invalid redirect URI: ${normalizedUri}`);
    }

    // ✅ Proceed with token exchange
    const tokenUrl = 'https://oauth2.googleapis.com/token';
    
    const params = {
      code,
      client_id: this.config.clientId,
      client_secret: this.config.clientSecret,
      redirect_uri: normalizedUri,
      grant_type: 'authorization_code'
    };

    try {
      const response = await axios.post(tokenUrl, qs.stringify(params), {
        headers: {
          'Content-Type': 'application/x-www-form-urlencoded'
        }
      });

      return response.data;
    } catch (error) {
      console.error('Token exchange failed:', error.response?.data || error.message);
      throw new Error('Failed to exchange authorization code for tokens');
    }
  }

  // ✅ Validate OAuth state parameter
  validateState(storedState, receivedState) {
    return storedState && receivedState && storedState === receivedState;
  }
}

// ✅ Initialize advanced service
const advancedOAuthService = new AdvancedGoogleOAuthService();

Solution 5: Error Handling and Recovery

❌ Without Proper Error Recovery:

// ❌ Basic error handling
app.get('/auth/google/callback', async (req, res) => {
  const { code } = req.query;
  
  try {
    // ❌ Generic error handling
    const tokens = await exchangeCodeForTokens(code, REDIRECT_URI);
    res.json(tokens);
  } catch (error) {
    res.status(400).json({ error: 'Authentication failed' }); // ❌ Generic message
  }
});

✅ With Proper Error Recovery:

Comprehensive Error Handling:

// ✅ Comprehensive Google OAuth error handling and recovery
class GoogleOAuthErrorHandler {
  constructor() {
    this.errorCodes = {
      'redirect_uri_mismatch': {
        message: 'Redirect URI does not match registered URIs',
        suggestion: 'Check Google Cloud Console registered redirect URIs match your application URIs'
      },
      'invalid_grant': {
        message: 'Authorization code is invalid or expired',
        suggestion: 'Try signing in again'
      },
      'unauthorized_client': {
        message: 'Client is not authorized to use this authentication method',
        suggestion: 'Check OAuth client configuration in Google Cloud Console'
      },
      'access_denied': {
        message: 'User denied access',
        suggestion: 'User cancelled the authentication process'
      }
    };
  }

  // ✅ Handle OAuth errors with appropriate recovery strategies
  handleOAuthError(error, context = {}) {
    console.error('Google OAuth Error:', {
      error: error.message || error,
      context: context,
      timestamp: new Date().toISOString()
    });

    // ✅ Determine error type and provide specific guidance
    const errorType = this.determineErrorType(error);
    const errorInfo = this.getErrorInfo(errorType);

    // ✅ Log for monitoring
    this.logError(error, context, errorType);

    // ✅ Return structured error response
    return {
      error: errorType,
      message: errorInfo.message,
      suggestion: errorInfo.suggestion,
      details: error.message || error.toString()
    };
  }

  // ✅ Determine error type from response
  determineErrorType(error) {
    const errorMessage = error.message || error.toString();

    if (errorMessage.includes('redirect_uri_mismatch')) {
      return 'redirect_uri_mismatch';
    } else if (errorMessage.includes('invalid_grant')) {
      return 'invalid_grant';
    } else if (errorMessage.includes('unauthorized_client')) {
      return 'unauthorized_client';
    } else if (errorMessage.includes('access_denied')) {
      return 'access_denied';
    } else if (errorMessage.includes('400') || errorMessage.includes('Bad Request')) {
      return 'bad_request';
    } else if (errorMessage.includes('401') || errorMessage.includes('Unauthorized')) {
      return 'unauthorized';
    } else {
      return 'unknown_error';
    }
  }

  // ✅ Get error information
  getErrorInfo(errorType) {
    return this.errorCodes[errorType] || {
      message: 'An unknown error occurred',
      suggestion: 'Please try again or contact support'
    };
  }

  // ✅ Log error for monitoring
  logError(error, context, errorType) {
    console.log('OAuth Error Log:', {
      type: errorType,
      error: error.message || error,
      context: context,
      timestamp: new Date().toISOString()
    });
  }

  // ✅ Generate user-friendly error message
  generateUserMessage(errorType) {
    const messages = {
      'redirect_uri_mismatch': 'There is a configuration issue with the sign-in process. Please contact support.',
      'invalid_grant': 'The sign-in link has expired. Please try signing in again.',
      'unauthorized_client': 'The application is not properly configured for authentication.',
      'access_denied': 'Sign-in was cancelled. You can try again.',
      'bad_request': 'There was an issue with the sign-in request.',
      'unauthorized': 'Authentication failed. Please try again.',
      'unknown_error': 'An unexpected error occurred during sign-in.'
    };

    return messages[errorType] || messages['unknown_error'];
  }
}

// ✅ Initialize error handler
const oauthErrorHandler = new GoogleOAuthErrorHandler();

Working Code Examples

Complete Frontend Implementation:

// google-oauth-frontend.js
class GoogleOAuthFrontend {
  constructor() {
    this.service = new FrontendGoogleOAuthService();
    this.errorHandler = new GoogleOAuthErrorHandler();
  }

  // ✅ Initiate Google OAuth flow
  async initiateAuth() {
    try {
      await this.service.initiateAuth();
    } catch (error) {
      const errorResponse = this.errorHandler.handleOAuthError(error, {
        action: 'initiate_auth',
        timestamp: new Date().toISOString()
      });
      
      console.error('OAuth initiation failed:', errorResponse);
      throw error;
    }
  }

  // ✅ Handle OAuth callback
  async handleCallback(searchParams) {
    const urlParams = new URLSearchParams(searchParams);
    const code = urlParams.get('code');
    const state = urlParams.get('state');
    const error = urlParams.get('error');

    try {
      // ✅ Handle OAuth errors
      if (error) {
        const errorResponse = this.errorHandler.handleOAuthError(
          new Error(`OAuth error: ${error}`),
          { error, action: 'callback' }
        );
        
        console.error('OAuth callback error:', errorResponse);
        return { success: false, error: errorResponse };
      }

      // ✅ Handle successful callback
      if (code && state) {
        const tokens = await this.service.handleCallback(code, state);
        return { success: true, tokens };
      }

      // ✅ No code or state provided
      return { 
        success: false, 
        error: { message: 'No authorization code received' } 
      };
    } catch (error) {
      const errorResponse = this.errorHandler.handleOAuthError(error, {
        action: 'handle_callback',
        code: code ? 'present' : 'missing',
        state: state ? 'present' : 'missing'
      });
      
      console.error('OAuth callback failed:', errorResponse);
      return { success: false, error: errorResponse };
    }
  }

  // ✅ Check if user is authenticated
  isAuthenticated() {
    return this.service.areTokensValid();
  }

  // ✅ Get access token
  getAccessToken() {
    return this.service.getAccessToken();
  }

  // ✅ Clear authentication
  clearAuth() {
    this.service.clearTokens();
  }
}

// ✅ Initialize frontend service
const googleOAuthFrontend = new GoogleOAuthFrontend();

// ✅ Export for use in other modules
window.GoogleOAuthFrontend = googleOAuthFrontend;

Complete Backend Implementation:

// google-oauth-backend.js
const express = require('express');
const cors = require('cors');
const session = require('express-session');

const app = express();

// ✅ Middleware
app.use(cors());
app.use(express.json());
app.use(session({
  secret: process.env.SESSION_SECRET || 'your-session-secret',
  resave: false,
  saveUninitialized: true,
  cookie: { secure: process.env.NODE_ENV === 'production' }
}));

// ✅ Initialize advanced OAuth service
const advancedOAuthService = new AdvancedGoogleOAuthService();
const errorHandler = new GoogleOAuthErrorHandler();

// ✅ OAuth initiation endpoint
app.get('/auth/google', (req, res) => {
  const clientId = process.env.GOOGLE_CLIENT_ID;
  const redirectUri = req.query.redirect_uri || advancedOAuthService.getAllowedRedirectUris()[0];
  
  // ✅ Validate redirect URI
  if (!advancedOAuthService.validateRedirectUri(redirectUri)) {
    return res.status(400).json({
      error: 'Invalid redirect URI',
      allowedUris: advancedOAuthService.getAllowedRedirectUris()
    });
  }

  // ✅ Store redirect URI in session for security
  req.session.oauthRedirectUri = redirectUri;

  // ✅ Build authorization URL
  const authUrl = new URL('https://accounts.google.com/o/oauth2/v2/auth');
  authUrl.searchParams.append('client_id', clientId);
  authUrl.searchParams.append('redirect_uri', redirectUri);
  authUrl.searchParams.append('response_type', 'code');
  authUrl.searchParams.append('scope', 'email profile');
  authUrl.searchParams.append('access_type', 'offline');
  authUrl.searchParams.append('prompt', 'consent');

  // ✅ Add state parameter for security
  const state = Math.random().toString(36).substring(2, 15);
  req.session.oauthState = state;
  authUrl.searchParams.append('state', state);

  res.redirect(authUrl.toString());
});

// ✅ OAuth callback endpoint
app.get('/auth/google/callback', async (req, res) => {
  const { code, state, error } = req.query;
  const storedState = req.session.oauthState;
  const redirectUri = req.session.oauthRedirectUri;

  try {
    // ✅ Clear session data after use
    delete req.session.oauthState;
    delete req.session.oauthRedirectUri;

    // ✅ Check for OAuth errors
    if (error) {
      const errorResponse = errorHandler.handleOAuthError(
        new Error(`OAuth error: ${error}`),
        { error, action: 'callback' }
      );
      
      return res.status(400).json(errorResponse);
    }

    // ✅ Validate required parameters
    if (!code) {
      return res.status(400).json({
        error: 'missing_code',
        message: 'Authorization code is missing'
      });
    }

    if (!state || !storedState || state !== storedState) {
      return res.status(400).json({
        error: 'invalid_state',
        message: 'Invalid state parameter - possible CSRF attack'
      });
    }

    if (!redirectUri) {
      return res.status(400).json({
        error: 'missing_redirect_uri',
        message: 'Redirect URI is missing'
      });
    }

    // ✅ Validate redirect URI
    if (!advancedOAuthService.validateRedirectUri(redirectUri)) {
      return res.status(400).json({
        error: 'invalid_redirect_uri',
        message: 'Redirect URI does not match registered URIs',
        allowedUris: advancedOAuthService.getAllowedRedirectUris()
      });
    }

    // ✅ Exchange code for tokens
    const tokens = await advancedOAuthService.exchangeCode(code, redirectUri);

    // ✅ Get user info
    const userInfo = await getUserInfo(tokens.access_token);

    // ✅ Create session
    req.session.user = {
      id: userInfo.id,
      email: userInfo.email,
      name: userInfo.name,
      picture: userInfo.picture
    };

    // ✅ Redirect to frontend success page
    const frontendUrl = process.env.FRONTEND_URL || 'http://localhost:3000';
    res.redirect(`${frontendUrl}/auth/success?email=${encodeURIComponent(userInfo.email)}`);
  } catch (error) {
    const errorResponse = errorHandler.handleOAuthError(error, {
      action: 'callback_processing',
      code: code ? 'present' : 'missing',
      state: state ? 'present' : 'missing',
      redirectUri: redirectUri
    });

    console.error('OAuth callback error:', errorResponse);
    res.status(400).json(errorResponse);
  }
});

// ✅ Start server
const PORT = process.env.PORT || 3001;
app.listen(PORT, () => {
  console.log(`Google OAuth server running on port ${PORT}`);
});

Best Practices for Google OAuth

1. Secure Redirect URI Configuration

// ✅ Always register exact redirect URIs in Google Cloud Console
// ✅ Use HTTPS in production
// ✅ Include all possible redirect URIs for different environments

2. Proper State Parameter Usage

// ✅ Always use state parameter for CSRF protection
// ✅ Generate random state values
// ✅ Validate state parameter in callback

3. Environment-Specific Configuration

// ✅ Use different redirect URIs for different environments
// ✅ Validate configuration before deployment
// ✅ Test OAuth flow in each environment

4. Error Handling

// ✅ Implement comprehensive error handling
// ✅ Provide user-friendly error messages
// ✅ Log errors for monitoring and debugging

5. Security Measures

// ✅ Use HTTPS for all OAuth communications
// ✅ Validate redirect URIs server-side
// ✅ Implement proper session management

Debugging Steps

Step 1: Check Google Cloud Console Configuration

# ✅ Verify registered redirect URIs match your application
# ✅ Check for trailing slashes or missing protocols
# ✅ Ensure all environment URIs are registered

Step 2: Verify Redirect URI Format

// ✅ Check that redirect URI matches exactly what's registered
// ✅ Ensure proper protocol (http vs https)
// ✅ Verify no trailing slashes unless registered
const redirectUri = 'http://localhost:3000/auth/google/callback'; // Must match exactly

Step 3: Test OAuth Flow

# ✅ Use Google OAuth 2.0 Playground to test configuration
# ✅ Verify the authorization URL is constructed correctly
# ✅ Check that callback URL receives the authorization code

Step 4: Check Network Requests

// ✅ Monitor network requests in browser dev tools
// ✅ Verify the redirect URI in the authorization request
// ✅ Check the callback URL for errors

Common Mistakes to Avoid

1. Trailing Slashes

// ❌ Don't include trailing slashes unless registered
const redirectUri = 'http://localhost:3000/callback/'; // ❌ Wrong if not registered with slash

// ✅ Register and use URIs consistently
const redirectUri = 'http://localhost:3000/callback'; // ✅ Right

2. Protocol Mismatches

// ❌ Don't mix HTTP and HTTPS
// Register: https://yourdomain.com/callback
// Use: http://yourdomain.com/callback // ❌ Wrong

// ✅ Match protocol exactly
// Register: https://yourdomain.com/callback
// Use: https://yourdomain.com/callback // ✅ Right

3. Port Number Issues

// ❌ Don't forget to register specific ports
const redirectUri = 'http://localhost:3001/callback'; // ❌ If only 3000 is registered

// ✅ Register all ports you'll use
// Register: http://localhost:3000/callback, http://localhost:3001/callback

4. Domain Variations

// ❌ Don't assume www and non-www are the same
// Register: https://yourdomain.com/callback
// Use: https://www.yourdomain.com/callback // ❌ Wrong

// ✅ Register all domain variations you'll use
// Register: https://yourdomain.com/callback, https://www.yourdomain.com/callback

Performance Considerations

1. Efficient URI Validation

// ✅ Cache allowed redirect URIs for performance
// ✅ Use efficient data structures for validation

2. Minimize Network Requests

// ✅ Validate configuration before making OAuth requests
// ✅ Cache validation results when appropriate

3. Optimize Error Handling

// ✅ Use efficient error categorization
// ✅ Implement proper logging without performance impact

Security Considerations

1. Protect Against CSRF

// ✅ Always use state parameter
// ✅ Validate state parameter in callback
// ✅ Use secure session management

2. Secure Token Storage

// ✅ Store tokens securely (preferably server-side)
// ✅ Use HTTPS for all communications
// ✅ Implement proper token expiration handling

3. Input Validation

// ✅ Validate all OAuth parameters
// ✅ Sanitize redirect URIs
// ✅ Implement proper error handling

Testing OAuth Configuration

1. Unit Tests for URI Validation

// ✅ Test redirect URI validation with various inputs
test('should validate correct redirect URI', () => {
  const service = new AdvancedGoogleOAuthService();
  const isValid = service.validateRedirectUri('http://localhost:3000/auth/google/callback');
  expect(isValid).toBe(true);
});

2. Integration Tests for OAuth Flow

// ✅ Test complete OAuth flow with valid configuration
test('should complete OAuth flow with valid configuration', async () => {
  // Test the complete flow
});

3. Error Handling Tests

// ✅ Test error handling for various scenarios
test('should handle redirect_uri_mismatch error', () => {
  // Test error handling
});

Alternative Solutions

1. Google Identity Services

// ✅ Consider Google Identity Services for newer implementations
// ✅ Better security and user experience

2. OAuth Libraries

// ✅ Use established OAuth libraries for complex scenarios
// ✅ Passport.js, Google Auth Library, etc.

3. Custom Authentication

// ✅ Implement custom solutions for specific requirements
// ✅ Add additional security layers when needed

Migration Checklist

  • Register all required redirect URIs in Google Cloud Console
  • Verify protocol (HTTP/HTTPS) matches registered URIs
  • Check for trailing slashes in redirect URIs
  • Test OAuth flow in all environments (dev, staging, prod)
  • Implement proper error handling and user feedback
  • Add security measures (state parameter, CSRF protection)
  • Update documentation for team members
  • Test with various redirect URI scenarios

Conclusion

The ‘Google OAuth redirect_uri_mismatch’ error is a common but manageable authentication issue that occurs when the redirect URI in your OAuth request doesn’t match the authorized redirect URIs configured in your Google Cloud Console. By following the solutions provided in this guide—whether through proper Google Cloud Console configuration, environment-specific handling, secure implementation, or comprehensive error handling—you can create robust and secure Google OAuth integration systems.

The key is to ensure exact matching between your application’s redirect URIs and those registered in Google Cloud Console, implement proper error handling, use security best practices like state parameters, and test thoroughly across all environments. With proper implementation of these patterns, your applications will provide a seamless Google authentication experience while maintaining security.

Remember to always register exact redirect URIs in Google Cloud Console, implement proper state parameter validation, handle errors gracefully, and test thoroughly to create secure and user-friendly applications that properly integrate with Google OAuth.

Gautam Sharma

About Gautam Sharma

Full-stack developer and tech blogger sharing coding tutorials and best practices

Related Articles

Tutorials

Fix: 401 Unauthorized Error - Complete Guide to Authentication Issues

Complete guide to fix 401 Unauthorized errors. Learn how to resolve authentication issues with practical solutions, token management, and best practices for secure API communication.

January 8, 2026
Tutorials

How to Fix: API Key Not Working error - Full Tutorial

Complete guide to fix API key not working errors. Learn how to resolve authentication issues with practical solutions, key management, and best practices for secure API communication.

January 8, 2026
Tutorials

Fix: JWT token invalid or expired error - Complete Guide

Complete guide to fix JWT token invalid or expired errors. Learn how to handle JWT authentication issues with practical solutions, token refresh strategies, and best practices for secure applications.

January 8, 2026