signInWithCustomToken failed to fetch error
Opened this issue · 1 comments
Operating System
Mac OS X >=10.15.7, Windows >=10
Environment (if applicable)
Chrome, Safari, Firefox
Firebase SDK Version
^12.1.0
Firebase SDK Product(s)
Auth
Project Tooling
- Next.js 14 with TypeScript
- Firebase ^12.1.0
- Clerk authentication
- Client-side React components
Detailed Problem Description
There is an issue with the signInWithCustomToken function in the client side firebase npm package. We simply want to use a custom token to authenticate with firestore, what occurs to 5% of the users randomly for ~1 hour, before self resolving.
To better describe, what's occurring is:
We first get an authentication token from clerk. Verify the token to be valid, await for the firebase auth state to be ready (configured to have in memory persistence to avoid local indexedDb issues), then simply pass the token into the function: signInWithCustomToken. What subsequently happens is that, after 30 seconds of doing nothing, the signInWithCustomToken throws a failed to fetch error. Once this occurs the user isn't able to authenticate via the signInWithCustomToken for an unknown amount of time, usually around one hour.
Why It Must Be the Sign-In Function
- Initialization is Working
getBrowserAuth() successfully returns auth instance
auth.authStateReady() completes without timeout
firebaseAuth state is properly set and available - Token Generation is Working
getToken({ template: 'integration_firebase' }) returns non-null token
Token validation passes (no null check failures)
Clerk session is active and valid
verified with the clerk team that the token is valid
signing in with the SAME token on other devices works - Prerequisites Are Met
isLoaded && isSignedIn && user all true
isFirebaseAuthReady is true (auth listener established)
!isAuthenticating prevents race conditions - Error Pattern
Error consistently occurs during signInWithFirebase() call
Firebase auth object exists and is initialized
Token exists and is generated successfully - No network requests
despite setting the persistence level of firebase/auth to in memory,
the signInWithFirebase function doesn't send network requests when this error occurs.
While the error is not reproducible, but this is the minimal code to get it to occur
import { useAuth } from '@clerk/nextjs';
import { auth } from '@/lib/firebase';
import { signInWithCustomToken } from 'firebase/auth';
const performAuth = async () => {
const { getToken, isLoaded, isSignedIn, userId } = useAuth();
// Wait for Clerk to be fully loaded
if (!isLoaded || !isSignedIn || !userId) {
return false;
}
try {
// Get Firebase token from Clerk
const token = await getToken({ template: 'integration_firebase' });
if (!token) {
throw new Error('Token unavailable');
}
// Sign in to Firebase with custom token - THIS IS WHERE IT HANGS
const credentials = await signInWithCustomToken(auth, token);
return !!credentials.user;
} catch (err) {
throw new Error('Authentication failed');
}
};
Steps and code to reproduce issue
This is our code (slightly modified) that we use in production:
- We do a lot of work to ensure that the auth is properly initialized on the local version.
- the only insight I can give into the issue is that previously, in the above code, if the firebase auth wasn't properly initialized before calling the signInWithCustomToken was called, then the issue also occurs in a similar fashion.
'use client';
import { createContext, useContext, useEffect, useState } from 'react';
import { useAuth, useUser } from '@clerk/nextjs';
import { signInWithFirebase } from '@/lib/firebase/auth';
import { onAuthStateChanged, getAuth, initializeAuth, inMemoryPersistence, type Auth } from 'firebase/auth';
import { app } from '@/lib/firebase';
import * as Sentry from '@sentry/nextjs';
let authSingleton: Auth | null = null;
export function getBrowserAuth(): Auth {
if (typeof window === 'undefined') {
throw new Error('getBrowserAuth can only be called on the client side');
}
if (!authSingleton) {
try {
authSingleton = initializeAuth(app, { persistence: inMemoryPersistence });
} catch {
// initializeAuth throws if already initialized; fall back to getAuth
authSingleton = getAuth(app);
}
}
return authSingleton;
}
interface FirebaseAuthContextType {
isFirebaseAuthenticated: boolean;
firebaseAuthError: Error | null;
isAuthenticating: boolean;
isFirebaseAuthReady: boolean;
firebaseAuth: Auth | null;
}
const FirebaseAuthContext = createContext<FirebaseAuthContextType | undefined>(undefined);
const FIREBASE_AUTH_TIMEOUT = 3000;
const FIREBASE_SETUP_TIMEOUT = 5000;
export function FirebaseAuthProvider({ children }: { children: React.ReactNode }) {
const { isSignedIn, user } = useUser();
const { getToken, isLoaded } = useAuth();
const [isFirebaseAuthenticated, setIsFirebaseAuthenticated] = useState(false);
const [firebaseAuthError, setFirebaseAuthError] = useState<Error | null>(null);
const [isAuthenticating, setIsAuthenticating] = useState(false);
const [isFirebaseAuthReady, setIsFirebaseAuthReady] = useState(false);
const [firebaseAuth, setFirebaseAuth] = useState<Auth | null>(null);
// Initialize Firebase auth state listener and ensure readiness
useEffect(() => {
if (typeof window === 'undefined') return;
let timeoutId: ReturnType<typeof setTimeout>;
let unsubscribe: (() => void) | undefined;
const setupAuthListener = async () => {
try {
const auth = getBrowserAuth();
setFirebaseAuth(auth);
const startTime = Date.now();
// Race between auth.authStateReady() and timeout
const timeoutPromise = new Promise<never>((_, reject) => {
setTimeout(() => {
const duration = Date.now() - startTime;
reject(new Error(`Firebase auth initialization timed out after ${duration}ms`));
}, FIREBASE_AUTH_TIMEOUT);
});
try {
await Promise.race([auth.authStateReady(), timeoutPromise]);
} catch (authReadyError) {
console.error('Firebase auth initialization failed:', authReadyError);
throw authReadyError;
}
unsubscribe = onAuthStateChanged(auth, (firebaseUser) => {
if (!firebaseUser) {
setIsFirebaseAuthenticated(false);
setFirebaseAuthError(null);
}
if (!isFirebaseAuthReady) {
setIsFirebaseAuthReady(true);
if (timeoutId) {
clearTimeout(timeoutId);
}
}
});
} catch (error) {
console.error('Failed to set up Firebase auth listener:', error);
setFirebaseAuthError(error instanceof Error ? error : new Error('Failed to initialize Firebase auth'));
// Fallback: proceed after timeout even if setup fails
timeoutId = setTimeout(() => {
console.warn('Firebase auth setup failed, proceeding with fallback initialization');
setIsFirebaseAuthReady(true);
}, FIREBASE_AUTH_TIMEOUT);
}
};
setupAuthListener();
// Ultimate fallback timeout
timeoutId = setTimeout(() => {
if (!isFirebaseAuthReady) {
console.warn('Firebase auth setup did not complete within expected timeframe');
}
}, FIREBASE_SETUP_TIMEOUT);
return () => {
if (unsubscribe) {
unsubscribe();
}
if (timeoutId) {
clearTimeout(timeoutId);
}
};
}, []);
// Reset authentication state when user changes
useEffect(() => {
if (user?.id) {
setIsFirebaseAuthenticated(false);
setFirebaseAuthError(null);
}
}, [user?.id]);
// Handle Firebase authentication when Clerk user is authenticated
useEffect(() => {
const shouldAuthenticate = isLoaded &&
isSignedIn &&
user &&
!isFirebaseAuthenticated &&
!isAuthenticating &&
isFirebaseAuthReady;
if (shouldAuthenticate) {
const authenticateWithFirebase = async () => {
setIsAuthenticating(true);
setFirebaseAuthError(null);
try {
const token = await getToken({ template: 'integration_firebase' });
if (!token) {
throw new Error('Firebase integration token is null - authentication may have failed');
}
if (!firebaseAuth) {
throw new Error('Firebase auth instance not available');
}
const firebaseUser = await signInWithFirebase(firebaseAuth, () => Promise.resolve(token));
if (firebaseUser) {
setIsFirebaseAuthenticated(true);
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown authentication error';
console.error('Firebase authentication failed:', errorMessage);
setFirebaseAuthError(error instanceof Error ? error : new Error('Firebase authentication failed'));
} finally {
setIsAuthenticating(false);
}
};
authenticateWithFirebase();
}
}, [isLoaded, isSignedIn, user?.id, isFirebaseAuthenticated, isAuthenticating, isFirebaseAuthReady, firebaseAuth, getToken]);
return (
<FirebaseAuthContext.Provider
value={{
isFirebaseAuthenticated,
firebaseAuthError,
isAuthenticating,
isFirebaseAuthReady,
firebaseAuth,
}}
>
{children}
</FirebaseAuthContext.Provider>
);
}
export function useFirebaseAuth() {
const context = useContext(FirebaseAuthContext);
if (context === undefined) {
throw new Error('useFirebaseAuth must be used within a FirebaseAuthProvider');
}
return context;
}
export function useFirebaseAuthInstance() {
const { firebaseAuth, isFirebaseAuthReady } = useFirebaseAuth();
return { firebaseAuth, isFirebaseAuthReady };
}
here is the signInWith custom token using the auth object created in the above:
import { signInWithCustomToken } from 'firebase/auth';
import type { Auth } from 'firebase/auth';
export async function signInWithFirebase(auth: Auth, getToken: () => Promise<string | null>) {
// ✅ CRITICAL: Ensure this only runs on client side
if (typeof window === 'undefined') {
throw new Error('signInWithFirebase can only be called on the client side');
}
const token = await getToken();
if (!token) {
throw new Error('No Firebase token available from Clerk');
}
try {
const userCredentials = await signInWithCustomToken(auth, token);
return userCredentials.user;
} catch (error) {
console.error('Firebase auth sign-in failed:', error);
throw new Error(`Firebase authentication failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
I couldn't figure out how to label this issue, so I've labeled it for a human to triage. Hang tight.