Supabase returns 'bad_oauth_callback: OAuth callback contains an error' when the OAuth provider redirects back with an error parameter in the URL. This typically happens when the user denies permission, the OAuth flow times out, or there's a configuration mismatch between your Supabase project and the OAuth provider.
The error "bad_oauth_callback: OAuth callback contains an error" occurs when Supabase's Auth API receives a callback from an OAuth provider (like Google, GitHub, or Facebook) that includes an error parameter in the query string. This indicates the OAuth flow failed before Supabase could exchange the authorization code for tokens. When users initiate OAuth sign-in, they're redirected to the provider's authorization page. If they deny permission, cancel the flow, or if the provider encounters an issue, the provider redirects back to your callback URL with error parameters like "error=access_denied" or "error=invalid_request". Supabase detects these error parameters and returns the unified "bad_oauth_callback" message instead of proceeding with user creation or session establishment. This error protects your application from processing incomplete or failed OAuth attempts, but it requires you to handle the callback gracefully and guide users through troubleshooting the OAuth provider's specific issue.
When the error occurs, examine the full callback URL in the browser's address bar. Look for error parameters that give specific clues:
// Example callback URL with error details
// https://your-app.com/auth/callback?error=access_denied&error_description=The+user+denied+your+request
// Parse the URL to extract error information
const urlParams = new URLSearchParams(window.location.search);
const errorType = urlParams.get('error'); // e.g., "access_denied"
const errorDescription = urlParams.get('error_description');
console.log('OAuth error type:', errorType);
console.log('Error description:', errorDescription);Common error values include:
- access_denied: User clicked "Cancel" or denied permissions
- invalid_request: Missing or malformed parameters in the OAuth request
- unauthorized_client: Client ID not authorized for this flow
- unsupported_response_type: Response type not supported by provider
- server_error: Provider's internal error
This information helps you provide targeted guidance to users.
Check that your OAuth provider is properly configured in the Supabase dashboard:
1. Go to Authentication → Providers in your Supabase project dashboard
2. Select the OAuth provider (Google, GitHub, etc.)
3. Verify:
- Client ID and Client Secret are correct and not expired
- Redirect URL matches exactly what's configured in the OAuth provider's developer console
- Enabled toggle is switched on
- Required scopes are included (profile, email for most providers)
For Google OAuth specifically, ensure:
- The OAuth consent screen is configured and published (not in "Testing" mode if you have external users)
- The redirect URI is exactly: https://[project-ref].supabase.co/auth/v1/callback
- "Authorized JavaScript origins" includes your app's domain
For GitHub:
- Ensure the callback URL is: https://[project-ref].supabase.co/auth/v1/callback
- Check that you've requested the correct scopes (user:email, read:user)
When users deny permission, provide clear feedback and alternative sign-in options:
import { AuthError } from '@supabase/supabase-js'
// In your callback handler or error boundary
const handleAuthError = (error: AuthError) => {
if (error.message.includes('bad_oauth_callback')) {
const urlParams = new URLSearchParams(window.location.search)
const errorType = urlParams.get('error')
if (errorType === 'access_denied') {
// User explicitly denied permission
return {
title: 'Permission Required',
message: 'You need to grant permissions to sign in with this provider. Please try again and click "Allow" when prompted.',
action: 'Try Again'
}
} else {
// Other OAuth errors
return {
title: 'Sign-in Failed',
message: 'We couldn't sign you in with this provider. Please try another method or contact support.',
action: 'Use Email Instead'
}
}
}
}
// Clear the error from URL to prevent infinite loops
if (window.location.search.includes('error=')) {
window.history.replaceState({}, '', window.location.pathname)
}Always offer alternative sign-in methods (email/password, different OAuth provider) when one fails.
OAuth configuration often breaks between environments. Test systematically:
// Environment-specific configuration
const getSupabaseUrl = () => {
if (process.env.NODE_ENV === 'development') {
return process.env.NEXT_PUBLIC_SUPABASE_URL_LOCAL
}
if (process.env.VERCEL_ENV === 'preview') {
return process.env.NEXT_PUBLIC_SUPABASE_URL_STAGING
}
return process.env.NEXT_PUBLIC_SUPABASE_URL_PRODUCTION
}
// Verify the correct project reference is being used
console.log('Supabase project ref:', process.env.NEXT_PUBLIC_SUPABASE_URL?.split('.')[0])
// Test OAuth flow manually
const testOAuthFlow = async () => {
const { data, error } = await supabase.auth.signInWithOAuth({
provider: 'google',
options: {
redirectTo: window.location.origin + '/auth/callback',
queryParams: {
// Add test parameters if needed
prompt: 'select_account' // Forces account selection
}
}
})
if (error) {
console.error('OAuth initiation failed:', error)
// Check if it's a configuration issue
if (error.message.includes('Provider not enabled')) {
console.error('Provider not enabled in Supabase dashboard')
}
}
}Common issues:
- Localhost vs production domain mismatches in OAuth provider configuration
- Different project references between environments
- Missing or incorrect redirect URIs for each environment
Track OAuth failures to identify patterns:
// Server-side error logging
app.get('/auth/callback', async (req, res) => {
try {
const { data, error } = await supabase.auth.getSession()
if (error) {
// Log detailed OAuth error information
console.error('OAuth callback error:', {
error: error.message,
errorCode: error.code,
urlParams: req.query,
timestamp: new Date().toISOString(),
userAgent: req.headers['user-agent'],
ip: req.ip
})
// Send to error monitoring service
await sendToSentry('oauth_callback_error', {
error: error.message,
provider: req.query.provider,
error_type: req.query.error
})
// Redirect to error page with helpful message
return res.redirect('/auth/error?type=oauth&details=' + encodeURIComponent(error.message))
}
// Success - redirect to app
res.redirect('/dashboard')
} catch (err) {
console.error('Unexpected error in callback:', err)
res.redirect('/auth/error?type=unexpected')
}
})Monitor for:
- Spike in "access_denied" errors (might indicate confusing consent screen)
- Specific providers failing more than others
- Geographic patterns (some providers restricted in certain regions)
- Correlation with recent configuration changes
### OAuth 2.0 error parameters
Supabase follows the OAuth 2.0 specification for error responses. When an OAuth provider redirects back with an error, it includes these standard parameters:
- error: Required. Single ASCII error code from the OAuth 2.0 specification.
- error_description: Optional. Human-readable description for developers.
- error_uri: Optional. URI to a web page with more information.
- state: The original state parameter sent in the request (for correlation).
Supabase surfaces these as "bad_oauth_callback" to provide a consistent error interface, but you should parse the actual error parameters for specific troubleshooting.
### Provider-specific considerations
Google OAuth:
- Requires published OAuth consent screen for external users
- Has strict domain verification requirements
- May require additional scopes for certain user data
- Has rate limits that can cause temporary failures
GitHub OAuth:
- Organization restrictions may block sign-ins
- Requires email scope to access user email addresses
- May require organization approval for OAuth apps
Apple Sign In:
- Requires specific callback URL format
- Has strict privacy requirements
- May require additional configuration for web vs native apps
### Security implications
Never expose raw OAuth error details to end users in production, as they may reveal implementation details. Instead, log them server-side and show user-friendly messages. However, during development, displaying the specific error can speed up debugging.
Always validate the state parameter to prevent CSRF attacks, even when the callback contains an error. Supabase handles this automatically when you use their client library.
email_conflict_identity_not_deletable: Cannot delete identity because of email conflict
How to fix "Cannot delete identity because of email conflict" in Supabase
mfa_challenge_expired: MFA challenge has expired
How to fix "mfa_challenge_expired: MFA challenge has expired" in Supabase
conflict: Database conflict, usually related to concurrent requests
How to fix "database conflict usually related to concurrent requests" in Supabase
phone_exists: Phone number already exists
How to fix "phone_exists" in Supabase
StorageApiError: resource_already_exists
StorageApiError: Resource already exists