How to fix bad_oauth_state: OAuth state parameter is missing or invalid in Supabase

SupabaseINTERMEDIATEHIGHUpdated Dec 29, 2025· 227 views

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.

Quick fix

Fixes bad_oauth_state

https://myapp.com          (production domain)
https://app.example.com    (subdomain)
http://localhost:3000      (local development)
http://localhost:5173      (Vite dev server)
  1. Verify Site URL in Supabase Auth settings
  2. Ensure HTTP/HTTPS protocol matches
  3. Add all redirect URIs to Supabase Auth settings
  4. Use @supabase/ssr for automatic state handling (recommended)
supabase — error
https://myapp.com (production domain)
https://app.example.com (subdomain)
http://localhost:3000 (local development)
http://localhost:5173 (Vite dev server)
bad_oauth_state: OAuth state parameter is missing or invalid

What this error means

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

Typical symptoms

  • OAuth login redirects fail with HTTP 400 error
  • Error message: 'bad_oauth_state: OAuth state parameter is missing or invalid'
  • Authentication succeeds at the OAuth provider but fails on redirect back to your app
  • Login works in one environment (local/staging) but fails in another (production)
  • Works with localhost:3000 but fails with 127.0.0.1:3000
  • Intermittent failures, especially when using multiple tabs
  • Works fine with @supabase/auth-ui but fails with custom OAuth handling
  • State parameter appears blank or missing in the callback URL

Common causes

  1. Site URL in Supabase Authentication settings doesn't exactly match your application URL
  2. Protocol mismatch between Supabase config and actual application (http vs https)
  3. Using 127.0.0.1 in configuration but accessing via localhost or a domain name
  4. Browser blocking third-party cookies required for state parameter storage
  5. Application cookies being cleared between OAuth redirect and callback
  6. Multiple domain/subdomain configurations causing cookie scope issues
  7. OAuth provider's redirect URI not matching the one registered in Supabase
  8. Session storage not configured to persist across navigation
  9. Using old @supabase/supabase-js without proper session persistence settings
  10. PKCE flow disabled or misconfigured in Supabase client initialization

How to fix "bad_oauth_state: OAuth state parameter is missing or invalid"

1Verify Site URL in Supabase Auth settings

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:

bash
https://myapp.com          (production domain)
https://app.example.com    (subdomain)
http://localhost:3000      (local development)
http://localhost:5173      (Vite dev server)

Common mistakes:

bash
❌ 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.

2Ensure HTTP/HTTPS protocol matches

The protocol in your Supabase Site URL MUST match the protocol your application uses:

Correct:

bash
Site URL: https://myapp.com     → Access via https://myapp.com
Site URL: http://localhost:3000 → Access via http://localhost:3000

Incorrect:

bash
Site URL: https://myapp.com but access http://myapp.com
Site URL: http://localhost:3000 but access https://localhost:3000

For 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.

3Add all redirect URIs to Supabase Auth settings

Beyond the Site URL, explicitly register all OAuth callback URIs. Go to Authentication > URL Configuration > Redirect URLs and add:

bash
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.

4Use @supabase/ssr for automatic state handling (recommended)

The @supabase/ssr package handles state parameter storage, PKCE flow, and session persistence automatically. This is the recommended approach for any web application:

Install:

bash
npm install @supabase/ssr

Next.js example (App Router):

typescript
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

5Enable session persistence in @supabase/supabase-js client

If not using @supabase/ssr, configure your Supabase client to persist sessions:

Browser-based app:

typescript
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):

typescript
// 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.

6Check browser cookie settings

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:

typescript
// 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.

7Test OAuth flow with browser DevTools

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:

bash
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:

bash
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

8Use exchangeCodeForSession for manual callback handling

If implementing a custom OAuth callback handler, properly exchange the code:

Next.js example:

typescript
// 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:

typescript
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.

9Clear cookies and test in incognito mode

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.

Advanced notes

### 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):

typescript
// 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:

bash
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:

typescript
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.

Frequently asked questions

What does "bad_oauth_state" mean?
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.
How do I fix "bad_oauth_state"?
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.
What causes "bad_oauth_state"?
Site URL in Supabase Authentication settings doesn't exactly match your application URL Protocol mismatch between Supabase config and actual application (http vs https) Using 127.0.0.1 in configuration but accessing via localhost or a domain name
Is the "bad_oauth_state" error serious?
Yes. This is a high-severity error that usually blocks work until it is fixed, but the steps above resolve it.

Official resources & further reading