This error occurs during Supabase OAuth authentication when the state parameter used for CSRF protection is missing, invalid, or doesn't match between the initial request and OAuth provider callback. It typically indicates a session mismatch, hostname misconfiguration, or cookie persistence issue.
The OAuth state parameter is a critical security mechanism that prevents Cross-Site Request Forgery (CSRF) attacks. When you initiate an OAuth flow, Supabase generates a unique, random state value and stores it securely in a cookie or session storage. When the OAuth provider redirects back to your application, Supabase validates that the state parameter in the callback matches the one it generated. If the state parameter is missing, invalid, or doesn't match, Supabase rejects the authentication to protect your users from unauthorized login attempts. This error typically results in a 400 HTTP response with the error code "bad_oauth_state". Common causes include: - Site URL in Supabase settings not matching your application's actual URL - HTTP/HTTPS protocol mismatches - Browser cookies being blocked or cleared between redirect and callback - Using localhost in your browser but configuring 127.0.0.1 in Supabase (or vice versa) - Session storage not properly persisting the state value across the OAuth redirect
In your Supabase project dashboard, navigate to Authentication > URL Configuration and check the 'Site URL' field. This MUST exactly match how your users access your application:
Correct examples:
https://myapp.com (production domain)
https://app.example.com (subdomain)
http://localhost:3000 (local development)
http://localhost:5173 (Vite dev server)Common mistakes:
❌ Site URL: http://127.0.0.1:3000 but accessing via http://localhost:3000
❌ Site URL: https://myapp.com but running on http://myapp.local
❌ Site URL: https://myapp.com but accessing http://localhost:3000 for testing
❌ Site URL: https://www.myapp.com but accessing https://myapp.com (or vice versa)Note: If you need multiple Site URLs (local, staging, production), add them all to the Redirect URLs list instead.
The protocol in your Supabase Site URL MUST match the protocol your application uses:
Correct:
Site URL: https://myapp.com → Access via https://myapp.com
Site URL: http://localhost:3000 → Access via http://localhost:3000Incorrect:
Site URL: https://myapp.com but access http://myapp.com
Site URL: http://localhost:3000 but access https://localhost:3000For production: Always use HTTPS. Self-signed certificates for local development should use http:// instead.
If you're behind a reverse proxy or load balancer that terminates SSL, ensure the Site URL uses HTTPS even though your application server receives HTTP.
Beyond the Site URL, explicitly register all OAuth callback URIs. Go to Authentication > URL Configuration > Redirect URLs and add:
http://localhost:3000/auth/callback (local development)
http://localhost:3000 (if SPA redirects to root)
https://myapp.com/auth/callback (production)
https://staging.myapp.com/auth/callback (staging)Important: The redirect URI must:
- Match exactly (no trailing slashes unless your app expects them)
- Use the same protocol as your Site URL
- Include the path if your callback handler isn't at the root
If your OAuth provider (Google, GitHub, etc.) has its own redirect URI settings, ensure they match exactly as well.
The @supabase/ssr package handles state parameter storage, PKCE flow, and session persistence automatically. This is the recommended approach for any web application:
Install:
npm install @supabase/ssrNext.js example (App Router):
import { createServerClient } from '@supabase/ssr'
import { cookies } from 'next/headers'
export function createClient() {
const cookieStore = cookies()
return createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
getAll() {
return cookieStore.getAll()
},
setAll(cookiesToSet) {
try {
cookiesToSet.forEach(({ name, value, options }) =>
cookieStore.set(name, value, options)
)
} catch {
// Handle in middleware if needed
}
},
},
}
)
}Benefits:
- Automatically manages state parameter in secure cookies
- Handles PKCE flow (more secure than state alone)
- Session persistence across server/client
- No manual cookie configuration needed
- Better error handling
If not using @supabase/ssr, configure your Supabase client to persist sessions:
Browser-based app:
import { createClient } from '@supabase/supabase-js'
const supabase = createClient(
process.env.REACT_APP_SUPABASE_URL!,
process.env.REACT_APP_SUPABASE_ANON_KEY!,
{
auth: {
persistSession: true, // Enable session persistence
storage: window.localStorage, // Or sessionStorage
autoRefreshToken: true,
detectSessionInUrl: true, // Auto-detect from URL
},
}
)Server-side rendering (SSR/Node.js):
// Use custom storage adapter for cookies
const supabase = createClient(
process.env.SUPABASE_URL!,
process.env.SUPABASE_ANON_KEY!,
{
auth: {
storage: {
getItem: (key) => cookies[key],
setItem: (key, value) => { cookies[key] = value },
removeItem: (key) => { delete cookies[key] },
},
},
}
)Critical: The client MUST be able to store and retrieve the state parameter from cookies or localStorage. Without this, state validation will fail.
State parameter storage relies on cookies. Verify cookies are allowed:
User-side checks:
1. Open browser DevTools: F12 > Application > Cookies
2. Verify cookies exist for your domain (look for sb-*-auth-token)
3. Check if "Block all cookies" is disabled in browser settings
4. Disable browser extensions that block cookies (temporarily for testing)
Application-side checks:
// Verify cookies are being set
console.log('Cookies enabled:', navigator.cookieEnabled)
// Check if auth cookies exist
const cookies = document.cookie
console.log('Document cookies:', cookies)For development on localhost:
- Firefox: Cookies work by default
- Chrome: Cookies work by default
- Safari: May require explicit permission
If cookies are completely blocked, state parameter cannot be persisted and OAuth will always fail.
Use browser DevTools to diagnose state parameter issues:
Steps:
1. Open DevTools: F12 > Network tab
2. Initiate OAuth login in your application
3. Look for the initial redirect to OAuth provider (e.g., accounts.google.com)
4. Check the request URL for the state parameter:
https://accounts.google.com/o/oauth2/v2/auth?
client_id=...
&state=abc123xyz... ← State parameter should be here
&redirect_uri=...5. After OAuth provider login, look for the redirect back to your app
6. The redirect URL should contain the same state:
http://localhost:3000/auth/callback?code=...&state=abc123xyz...7. If state is missing or different, the error will occur
8. Check Console tab for error messages with details
If state is missing at the start:
- Your application isn't properly initiating OAuth flow
- Check if signInWithOAuth() is being called correctly
If state is different on callback:
- Session storage lost during redirect (cookies not persistent)
- Browser cleared cookies between requests
If implementing a custom OAuth callback handler, properly exchange the code:
Next.js example:
// app/auth/callback/route.ts
import { createServerClient } from '@supabase/ssr'
import { cookies } from 'next/headers'
import { NextRequest, NextResponse } from 'next/server'
export async function GET(request: NextRequest) {
const requestUrl = new URL(request.url)
const code = requestUrl.searchParams.get('code')
if (code) {
const supabase = createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
getAll() { return cookies().getAll() },
setAll(cookiesToSet) {
cookiesToSet.forEach(({ name, value, options }) =>
cookies().set(name, value, options)
)
},
},
}
)
// This exchanges the code and validates the state parameter
const { data, error } = await supabase.auth.exchangeCodeForSession(code)
if (error) {
// State validation failed - log for debugging
console.error('State validation error:', error.message)
return NextResponse.redirect(`${requestUrl.origin}/auth/error?error=${error.code}`)
}
// Success - redirect to home
return NextResponse.redirect(`${requestUrl.origin}/`)
}
// No code provided
return NextResponse.redirect(`${requestUrl.origin}/auth/error`)
}React example:
import { useNavigate, useSearchParams } from 'react-router-dom'
import { useEffect } from 'react'
import { supabase } from '@/lib/supabase'
export function CallbackPage() {
const navigate = useNavigate()
const [searchParams] = useSearchParams()
const code = searchParams.get('code')
useEffect(() => {
if (!code) {
navigate('/auth/error')
return
}
supabase.auth.exchangeCodeForSession(code)
.then(({ data, error }) => {
if (error) {
console.error('OAuth state error:', error.message)
navigate('/auth/error')
} else {
navigate('/')
}
})
}, [code, navigate])
return <div>Processing authentication...</div>
}The exchangeCodeForSession method automatically validates the state parameter internally. If it's invalid, an error is returned.
Stale or corrupted cookies can cause state validation failures. Test in a clean session:
Steps:
1. Open your app in a new Incognito/Private browser window (no stored cookies)
2. Attempt OAuth login again
3. If it works in incognito but fails in normal mode, the issue is stale cookies
To manually clear cookies:
1. Open DevTools: F12 > Application > Cookies
2. Right-click and delete all cookies for your domain
3. Hard refresh the page (Ctrl+Shift+R or Cmd+Shift+R)
4. Try OAuth login again
Permanent fix:
If clearing cookies consistently fixes the issue, the problem is likely:
- Session storage using wrong domain/path
- Cookies set with incompatible SameSite attribute
- Cookies set to secure (HTTPS) but testing over HTTP
Use @supabase/ssr to automatically handle cookie configuration correctly.
### PKCE Flow (More Secure Alternative to State)
Supabase recommends PKCE (Proof Key for Code Exchange) over traditional state parameter validation alone. The @supabase/ssr package uses PKCE by default.
How PKCE differs:
- State parameter still used for CSRF protection
- Additional "code verifier" prevents authorization code injection attacks
- More resilient to state parameter loss in certain scenarios
When to use PKCE:
- Mobile apps (recommended by OAuth 2.1 spec)
- Single-page applications (SPAs)
- Native applications
- Always, if possible (it's strictly more secure)
### Multi-Tab Authentication Issues
If users open OAuth login in multiple tabs, the first tab's state might be consumed before other tabs callback:
Problem:
1. User opens OAuth in tab A - state_A generated
2. User opens OAuth in tab B - state_B generated
3. Tab A callback arrives - state validated, session created
4. Tab B callback arrives - state_B already used/invalid
Solution:
- Use PKCE flow (more forgiving)
- Implement state parameter reuse handling in your callback
- Consider disabling multi-tab OAuth flows in your UI
### Subdomain and Cookie Scope Issues
If your app spans subdomains (api.example.com vs app.example.com):
// Set cookies to parent domain
const supabase = createClient(url, key, {
auth: {
storage: {
getItem: (key) => getCookie(key),
setItem: (key, value) => setCookie(key, value, {
domain: '.example.com' // Parent domain
}),
removeItem: (key) => removeCookie(key)
}
}
})### Reverse Proxy and Load Balancer Configuration
If behind a reverse proxy that terminates SSL:
Site URL: https://myapp.com (external URL with HTTPS)
Internal: http://app-server:3000 (internal without SSL)Supabase needs the external URL (what users see), not the internal one.
### Debugging State Parameter Issues
Enable debug logging:
import { createClient } from '@supabase/supabase-js'
const supabase = createClient(url, key, {
auth: {
persistSession: true,
detectSessionInUrl: true,
debug: true // Enable debug logging
}
})Check browser console for detailed state validation logs.
email_address_not_authorized: Email sending to this address is not authorized
Email address not authorized for sending in Supabase Auth
reauthentication_needed: Reauthentication required for security-sensitive actions
Reauthentication required for security-sensitive actions
no_authorization: No authorization header was provided
How to fix "no authorization header was provided" in Supabase
otp_expired: OTP has expired
How to fix 'otp_expired: OTP has expired' in Supabase
mfa_factor_not_found: MFA factor could not be found
How to fix "mfa_factor_not_found: MFA factor could not be found" in Supabase