No articles found
Try different keywords or browse our categories
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.
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_mismatcherror message from GoogleError 400: redirect_uri_mismatchin browser- OAuth flow interruption with error page
invalid_requestwith 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.
Related Articles
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.
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.
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.