-
Notifications
You must be signed in to change notification settings - Fork 490
Description
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
- 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 });
}
- 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
}
- 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.