How to fix flow_state_expired: PKCE flow state has expired in Supabase

SupabaseINTERMEDIATEHIGHUpdated Dec 29, 2025ยท 171 views

The PKCE flow state expires after 5 minutes of inactivity. This error occurs when users take too long to complete the OAuth authentication callback or attempt to use an expired authorization code. Complete the authentication flow within 5 minutes or restart the login process to fix this.

Quick fix

Fixes flow_state_expired

// User initiates login again
await supabase.auth.signInWithOAuth({
  provider: 'google',
  options: {
    redirectTo: `${window.location.origin}/auth/callback`,
  },
});
  1. Complete the authentication flow within 5 minutes
  2. Restart the authentication flow
  3. Verify OAuth redirect URL configuration
  4. Use @supabase/ssr for SSR applications
supabase โ€” error
// User initiates login again
await supabase.auth.signInWithOAuth({
provider: 'google',
options: {
redirectTo: `${window.location.origin}/auth/callback`,
},
});
flow_state_expired: PKCE flow state has expired

What this error means

The "PKCE flow state has expired" error occurs when the OAuth 2.0 PKCE (Proof Key for Code Exchange) authentication code or state parameter becomes invalid due to timeout. In Supabase, PKCE flow state is designed to expire after 5 minutes for security reasons. The authorization code can only be exchanged for an access token once and must be completed within this 5-minute window. If a user is redirected to the callback URL but the code exchange happens after the timeout, or if they attempt to reuse an already-exchanged code, Supabase rejects the request with this error. This is a security measure to protect against code interception attacks and unauthorized token exchanges. Unlike regular session timeouts, this error specifically indicates that the temporary authorization code used in the OAuth handshake has become invalid.

Typical symptoms

  • Users see "flow_state_expired" error after clicking OAuth login link
  • Authentication fails during email magic link callback after an extended delay
  • Error appears in browser console: "AuthApiError: Invalid flow state"
  • Users cannot log in even though the callback URL is being reached
  • Refreshing the page or retrying the login flow still shows the error
  • User was redirected to login callback but the exchange failed

Common causes

  1. User took longer than 5 minutes to complete the OAuth authorization flow (clicked link, filled form, confirmed, etc.)
  2. Authorization code was exchanged more than once (PKCE codes are single-use)
  3. OAuth redirect URL is misconfigured, causing delays in reaching the callback endpoint
  4. Browser tab was left idle for more than 5 minutes before completing the callback
  5. Network latency or server delay caused the code exchange request to arrive after the timeout window
  6. Different browser or device was used to complete the OAuth flow than the one that initiated it (state mismatch)

How to fix "flow_state_expired: PKCE flow state has expired"

1Complete the authentication flow within 5 minutes

The PKCE authorization code expires 5 minutes after the OAuth flow is initiated. Ensure users complete the entire process (click link, authorize, callback) within this window.

If using magic links, advise users to:
- Click the authentication link promptly after requesting it
- Complete any required authorization steps immediately
- Not leave the page idle during the callback process

2Restart the authentication flow

If the error occurs, the authorization code has expired and cannot be reused. Restart the entire login process:

javascript
// User initiates login again
await supabase.auth.signInWithOAuth({
  provider: 'google',
  options: {
    redirectTo: `${window.location.origin}/auth/callback`,
  },
});

For magic links:

javascript
await supabase.auth.signInWithOtp({
  email: 'user@example.com',
});
// User receives new magic link and must click it promptly
3Verify OAuth redirect URL configuration

Ensure your OAuth redirect URL is correctly configured in Supabase and matches your application:

1. Go to Supabase dashboard > Authentication > URL Configuration
2. Verify "Redirect URLs" includes your callback URL exactly as it appears in your app
3. For local development: http://localhost:3000/auth/callback
4. For production: https://yourdomain.com/auth/callback

Misconfigured URLs can cause unnecessary delays reaching the callback endpoint.

For OAuth providers (Google, GitHub, etc.), also verify:
- OAuth client ID and secret are correct
- Authorized redirect URIs include your Supabase callback URL

4Use @supabase/ssr for SSR applications

If building a server-side rendered (SSR) application, use the @supabase/ssr package which handles PKCE flow and session management automatically:

bash
npm install @supabase/ssr
typescript
import { createServerClient } from '@supabase/ssr';

const supabase = createServerClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL,
  process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY,
  {
    cookies: {
      getAll() { /* ... */ },
      setAll() { /* ... */ },
    },
  }
);

This package:
- Uses PKCE flow by default
- Automatically handles state parameter storage in cookies
- Manages session tokens securely
- Handles callback exchange server-side

5Properly exchange the authorization code

When handling the OAuth callback, ensure you exchange the code within the 5-minute window:

typescript
// In your callback route handler (/auth/callback)
const { data, error } = await supabase.auth.exchangeCodeForSession(code);

if (error) {
  // Log specific error
  console.error('Code exchange failed:', error.message);
  // Redirect user back to login
  return redirect('/login');
}

// Session is now valid, redirect to dashboard
return redirect('/dashboard');

Ensure:
- The callback handler executes immediately upon redirect
- No delays or middleware interferes with code exchange
- The code is extracted from the URL query parameter and passed correctly

Advanced notes

Understanding PKCE State Management: PKCE (Proof Key for Code Exchange) improves OAuth security by introducing a code verifier (randomly generated secret) and code challenge (hash of the verifier). Supabase stores the state parameter in the browser (localStorage or cookies for SSR) and validates it when the code is exchanged. If the browser is closed or cookies are cleared, the state is lost. For same-device flows (recommended for SPAs), the code verifier must be available on the same browser instance that initiated the flow. Network Considerations: In high-latency environments or with slow APIs, reaching the callback endpoint quickly becomes critical. If your callback handler has expensive operations (database lookups, external API calls), defer these until after the code exchange completes. For email magic links with delays in sending, consider increasing user communication about the 5-minute window. Cookie vs LocalStorage: For SSR applications, Supabase recommends storing session data in cookies (via @supabase/ssr), which are automatically sent with requests and survive page refreshes. LocalStorage is only suitable for client-side SPAs where the same JavaScript instance handles the callback. Device/Browser Mismatch: The PKCE flow requires the same device and browser throughout. If a user clicks a link on mobile and completes auth on desktop (or vice versa), the code verifier won't be available. This is intentional security behavior but may confuse users sharing links.

Frequently asked questions

What does "flow_state_expired" mean?
0 PKCE (Proof Key for Code Exchange) authentication code or state parameter becomes invalid due to timeout. In Supabase, PKCE flow state is designed to expire after 5 minutes for security reasons. The authorization code can only be exchanged for an access token once and must be completed within this 5-minute window.
How do I fix "flow_state_expired"?
The PKCE flow state expires after 5 minutes of inactivity. This error occurs when users take too long to complete the OAuth authentication callback or attempt to use an expired authorization code. Complete the authentication flow within 5 minutes or restart the login process to fix this.
What causes "flow_state_expired"?
User took longer than 5 minutes to complete the OAuth authorization flow (clicked link, filled form, confirmed, etc.) Authorization code was exchanged more than once (PKCE codes are single-use) OAuth redirect URL is misconfigured, causing delays in reaching the callback endpoint
Is the "flow_state_expired" 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