Skip to content

exchangeCodeForSession() doesn't update Authorization headers on iOS/mobile platforms #1566

@simplysparsh

Description

@simplysparsh

Bug: exchangeCodeForSession() doesn't update Authorization headers on iOS/mobile platforms

Summary

After successfully calling exchangeCodeForSession() during OAuth flow on iOS native (Capacitor), the Supabase client fails to update its internal HTTP Authorization headers, causing all subsequent database queries to fail with authentication errors. The session is stored correctly and can be retrieved via getSession(), but the client's REST API headers remain unset.

Environment

  • @supabase/supabase-js: v2.39.7
  • Platform: iOS native (Capacitor)
  • Framework: React 18.3.1 with Vite
  • Capacitor: v6.0.0
  • iOS Version: 16.4+ (tested on iPhone 15 Pro simulator and physical devices)
  • OAuth Providers: Google and Apple (both affected)

Steps to Reproduce

  1. Set up OAuth in a Capacitor iOS app:
// OAuth setup for native iOS
async function startOAuthFlow(provider: 'google' | 'apple') {
  const { data, error } = await supabase.auth.signInWithOAuth({
    provider,
    options: {
      redirectTo: 'yourapp://auth/callback',
      skipBrowserRedirect: true,
    },
  });
  
  // Open in-app browser
  await Browser.open({ url: data.url });
}
  1. Handle OAuth callback:
// Handle deep link callback
async function handleOAuthCallback(url: string) {
  const code = new URL(url).searchParams.get('code');
  
  // Exchange code for session
  const { error } = await supabase.auth.exchangeCodeForSession(code);
  // Success! Session is stored
}
  1. Attempt database query immediately after:
// This query will fail even though auth succeeded
const { data, error } = await supabase
  .from('profiles')
  .select('*')
  .eq('id', userId)
  .single();
  
// Error: User not authenticated / JWT expired

Expected Behavior

After exchangeCodeForSession() succeeds:

  • The session should be stored in storage (this works)
  • The Supabase client's HTTP headers should include Authorization: Bearer [TOKEN]
  • Database queries should work immediately with authenticated session

Actual Behavior

After exchangeCodeForSession() succeeds:

  • The session IS stored correctly in storage (confirmed)
  • The Supabase client's HTTP headers are NOT updated (problem)
  • All database queries fail with authentication errors
  • The client only works after a full page/app reload

Console Logs Demonstrating the Issue

TO JS {"url":"yourapp://auth/callback?code=XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX#"}
[log] - [AuthStore] onAuthStateChange: event='SIGNED_IN', session exists= true
[log] - [AuthStore] onAuthStateChange: User signed in via google
[log] - [AuthStore] Supabase client headers AFTER OAuth: {
  "hasAuthHeader": false,
  "authHeaderPrefix": "undefined...",
  "sessionToken": "[REDACTED_TOKEN]...",
  "tokensMatch": false
}
[log] - [ProfileService] Current session state: {
  "hasSession": true,
  "userId": "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX",
  "hasAccessToken": true,
  "tokenPrefix": "[REDACTED_TOKEN]...",
  "provider": "google"
}
[log] - [ProfileService] Supabase client headers: {
  "hasAuthHeader": false,
  "authHeaderPrefix": "undefined...",
  "apiKey": true
}
[error] - Error fetching user profile: {"code":"PGRST301","message":"JWT expired"}

Debug Code to Verify Issue

// Add this after exchangeCodeForSession() to see the problem
const { data: { session } } = await supabase.auth.getSession();
const currentHeaders = (supabase as any).rest?.headers;

console.log('Auth state after OAuth:', {
  hasValidSession: !!session,
  sessionToken: '[REDACTED_TOKEN]...',
  hasAuthHeader: !!currentHeaders?.Authorization,
  headerValue: currentHeaders?.Authorization?.substring(0, 30) + '...',
  tokensMatch: false // Would check if header contains the session token
});

// Output shows:
// hasValidSession: true (works)
// sessionToken: "[REDACTED_TOKEN]..." (exists)
// hasAuthHeader: false (PROBLEM)
// headerValue: "undefined..." (missing)
// tokensMatch: false (mismatch)

Root Cause Analysis

The Supabase JavaScript client updates its internal HTTP headers during:

  • Initial client creation (when a session exists in storage)
  • Page reloads (which reinitialize the client from storage)
  • Some auth operations (login with password, etc.)

However, it does NOT update headers after:

  • exchangeCodeForSession() on mobile/native platforms

This works on web because OAuth redirects cause a natural page reload, which reinitializes the client with headers from the stored session.

On native platforms without page reload, the client maintains stale headers from before authentication.

Current Workaround

Force a page reload immediately after OAuth on native platforms:

// In onAuthStateChange listener
if (isNativeIOS() && event === 'SIGNED_IN' && session.user.app_metadata?.provider !== 'email') {
  // Force reload to reinitialize Supabase client with proper headers
  window.location.reload();
  return;
}

This workaround is not ideal as it:

  • Disrupts user experience with a visible reload
  • Adds latency to the authentication flow
  • Is a hack for what should be handled internally

Suggested Fix

The exchangeCodeForSession() method should internally update the client's Authorization headers after successfully setting the session:

Impact

This bug affects:

  • All Capacitor/native mobile apps using OAuth authentication with Supabase
  • User experience: Users cannot access authenticated features without manual app restart
  • Onboarding flows: New users get stuck after OAuth sign-up
  • Production readiness: Makes OAuth unusable in production mobile apps

Reproducibility

  • Consistency: 100% reproducible
  • Platforms: iOS (confirmed), likely affects Android native as well
  • Providers: Both Google and Apple OAuth affected
  • Versions: Confirmed on latest @supabase/supabase-js (2.39.7)

This is a critical bug that makes OAuth authentication unusable in production Capacitor apps without workarounds.

Test Repository

I can provide a minimal reproduction repository if needed. The issue is consistent and easily reproducible with any Capacitor + Supabase OAuth setup.


Please let me know if you need any additional information or clarification. This is blocking OAuth authentication in our production iOS app.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions