Firebase Auth Expo: Email, Google, Apple + Routes
Firebase Auth on Expo works well when you treat it as a native-auth problem: email/password is the easy path, Google and Apple need console credentials plus a development build, and Expo Router needs a clear session gate so signed-in and signed-out stacks do not fight each other.
This guide walks through a practical firebase auth expo setup: email/password (and optional email verification), Google Sign-In, Sign in with Apple (iOS), guest/anonymous mode, Expo Router protected-route patterns, and the real error strings that burn hours. Soft note: Auth is already wired in the CodeBaseHub Expo starter kit—you still configure Firebase/Google/Apple credentials and ship a dev build; this is not zero-setup.
Key Takeaways
- Use a development build (
expo-dev-client,npx expo run:*, or EAS). Native Google Sign-In, Apple auth, and@react-native-firebase/*do not belong in Expo Go (Expo Google authentication). - Wire email/password, Google, Apple (iOS only), and optional guest/anonymous—then gate navigation with a session provider, not a separate middleware layer.
- Treat
auth/invalid-credentialas the modern generic failure for wrong email/password when Email Enumeration Protection is on (Firebase JS Auth reference). - Register every Android SHA-1 you ship with (local debug, EAS, Play App Signing) or Google Sign-In will fail on some builds (Expo Google guide).
- Enable Sign in with Apple capability (
ios.usesAppleSignIn) and rebuild; App Store review also expects Apple when you offer other third-party logins (expo-apple-authentication). - Prefer Expo Router
Stack.Protected(SDK 53+) or a layout-level session redirect; wait for auth to load before redirecting (Expo Router authentication).
What You Need Before Coding
Lock these prerequisites before you expect Google or Apple to work on a device.
1. Firebase project + Auth providers
In the Firebase console:
- Create (or open) a project and register iOS and Android apps with the same bundle ID / package name as your Expo
appconfig. - Enable Email/Password, Google, and Apple under Authentication → Sign-in method.
- Download
GoogleService-Info.plist(iOS) andgoogle-services.json(Android). Point Expo at them viaios.googleServicesFile/android.googleServicesFile(see Expo Google authentication).
2. A development build (not Expo Go)
Libraries that ship custom native code—@react-native-firebase/*, @react-native-google-signin/google-signin (or Nitro Google Sign-In), and production-faithful Apple auth—cannot be validated inside Expo Go. Expo's own Google guide states those libraries need a config plugin and a development build.
Plan on:
expo-dev-clientin the projectnpx expo prebuild(when using CNG) +npx expo run:ios/run:android, or EAS Build
3. Google Cloud / SHA-1 and Apple Developer
- Google (Android): add every SHA-1 you ship with (local, EAS, Play App Signing) in Firebase Project settings (Firebase Android FAQ; Expo Google guide). Keep
EXPO_PUBLIC_GOOGLE_WEB_CLIENT_ID(web client ID) for ID-token exchange. - Apple: enable Sign in with Apple on the App ID, configure Firebase's Apple provider, set
ios.usesAppleSignIn: true, rebuild (Firebase Apple; Expo AppleAuthentication).
Firebase Auth on Expo: setup checklist
Use this as a working checklist for firebase auth expo projects (and validate paths against your own repo or kit).
- Install / link native auth — Firebase Auth JS or
@react-native-firebase/auth, plus Google and Apple packages your stack requires; add config plugins; rebuild the native binary. - Drop config files —
google-services.json,GoogleService-Info.plist, and the Google web client ID env var your Google → Firebase credential flow needs. - Enable providers — Email/Password, Google, Apple in Firebase Auth; match package / bundle IDs exactly.
- Feature flags — turn providers on deliberately (examples below). Keep email verification off until you want the UX and email templates live.
- Auth context + router gate — one session source of truth; session-based redirect between an auth stack and the main app tabs.
- Test on a real build — email first, then Google on Android and iOS, then Apple on a physical iPhone / Simulator with capability enabled.
Feature flags you will actually use
In a kit-style Expo app, feature flags keep unfinished console work from shipping as half-broken buttons. A common pattern:
| Flag | Typical default | Meaning |
|---|---|---|
googleSignIn | true | Show Google button; requires native Google + Firebase Google provider |
appleSignIn | true (iOS only) | Show Apple button on iOS; hide or no-op on Android |
guestMode | true | Allow anonymous / guest sign-in |
autoGuestMode | false | If ever enabled, can skip the auth stack entirely—keep off unless that UX is intentional |
emailVerification | false | Code may exist; default OFF so you do not force verify-email flows before templates and deep links are ready |
Soft CTA: those patterns (email/password, Google, Apple, guest, verification behind a flag) are how Auth is wired in the Expo starter kit. You still own Firebase console setup and a native rebuild.
Email / Password (and Optional Verification)
Email/password is the fastest smoke test. Modular JS shape (React Native Firebase is similar):
import {
createUserWithEmailAndPassword,
signInWithEmailAndPassword,
sendPasswordResetEmail,
sendEmailVerification,
} from "firebase/auth";
import { auth } from "./firebase"; // your initialized Auth instance
export async function signUp(email: string, password: string) {
const cred = await createUserWithEmailAndPassword(auth, email, password);
// Only if FEATURES.emailVerification === true:
// await sendEmailVerification(cred.user);
return cred.user;
}
export async function signIn(email: string, password: string) {
const cred = await signInWithEmailAndPassword(auth, email, password);
return cred.user;
}
export async function resetPassword(email: string) {
await sendPasswordResetEmail(auth, email);
}
Persistence: use React Native Auth persistence (getReactNativePersistence + AsyncStorage/SecureStore). Do not invent a second "logged in" flag that disagrees with onAuthStateChanged.
Email verification: feature flag default OFF. Turn on only when templates, continue URLs, and product rules (block tabs vs soft nudge) are ready.
Password reset: a forgot-password screen calling sendPasswordResetEmail is enough for v1.
Google Sign-In on Expo (Dev Build Required)
Use a native Google library + config plugin + development build—not Expo Go (Using Google authentication):
- Install
@react-native-google-signin/google-signinorreact-native-nitro-google-signin; add the config plugin. - Place
google-services.json/GoogleService-Info.plistin app config. - Set
webClientIdfromEXPO_PUBLIC_GOOGLE_WEB_CLIENT_IDfor an ID token. GoogleAuthProvider.credential→signInWithCredential.- Rebuild after plugin or Google Services changes.
Credential handoff:
import { GoogleSignin } from "@react-native-google-signin/google-signin";
import { GoogleAuthProvider, signInWithCredential } from "firebase/auth";
import { auth } from "./firebase";
GoogleSignin.configure({
webClientId: process.env.EXPO_PUBLIC_GOOGLE_WEB_CLIENT_ID,
});
export async function signInWithGoogle() {
await GoogleSignin.hasPlayServices();
const response = await GoogleSignin.signIn();
const idToken = response.data?.idToken; // shape varies by library version
if (!idToken) throw new Error("Missing Google ID token");
const credential = GoogleAuthProvider.credential(idToken);
return signInWithCredential(auth, credential);
}
Pin library versions carefully; the invariant is native Google ID token → Firebase credential → session.
Sign in with Apple (iOS Only)
Apple auth on Expo uses expo-apple-authentication:
- Set
ios.usesAppleSignIn: true(and the Apple Authentication plugin where applicable). - Rebuild so the
com.apple.developer.applesigninentitlement is present. - Call
AppleAuthentication.signInAsync, pass a hashed nonce to Apple and the raw nonce to Firebase (Firebase Authenticate Using Apple).
import * as AppleAuthentication from "expo-apple-authentication";
import * as Crypto from "expo-crypto";
import { OAuthProvider, signInWithCredential } from "firebase/auth";
import { auth } from "./firebase";
export async function signInWithApple() {
const rawNonce = Crypto.randomUUID();
const hashedNonce = await Crypto.digestStringAsync(
Crypto.CryptoDigestAlgorithm.SHA256,
rawNonce
);
const apple = await AppleAuthentication.signInAsync({
requestedScopes: [
AppleAuthentication.AppleAuthenticationScope.FULL_NAME,
AppleAuthentication.AppleAuthenticationScope.EMAIL,
],
nonce: hashedNonce,
});
if (!apple.identityToken) throw new Error("Missing Apple identity token");
const provider = new OAuthProvider("apple.com");
const credential = provider.credential({
idToken: apple.identityToken,
rawNonce,
});
return signInWithCredential(auth, credential);
}
iOS-only: hide Apple on Android (isAvailableAsync()). If you offer Google on iOS, plan Sign in with Apple before App Store review.
Guest / Anonymous Auth
Anonymous sign-in (signInAnonymously) creates a real UID you can later link to email, Google, or Apple—useful for "try first" funnels.
guestMode: true— show "Continue as guest".autoGuestMode: false(keep this default) — do not auto-skip the auth stack; silent anonymous sessions complicate linking, analytics, and account deletion.
Upgrade guests with linkWithCredential. Link / unlink / delete often need a recent login—catch auth/requires-recent-login and prompt re-auth instead of showing a generic failure.
Protected Routes with Expo Router
Expo Router auth is a session gate, not separate middleware. Prefer:
Stack.Protected/Tabs.Protected+guard(Router v5 / SDK 53+) (Authentication, Protected routes), or- Layout
<Redirect />on older SDKs (redirects guide).
Kit-shaped route groups (validate against your tree)
src/app/(auth)/—login,signup,forgot-passwordsrc/app/(tabs)/— signed-in UI- Gate:
AuthProvider(src/contexts/auth-context.tsx) + rootStack(src/app/_layout.tsx) / entry redirect
That is session-based redirect between auth stack and tabs—not custom middleware. Validate paths against your kit; do not invent deeper routes.
Illustrative root navigator (SDK 53+):
import { Stack } from "expo-router";
import { useAuth } from "@/contexts/auth-context"; // validate import path
function RootNavigator() {
const { user, isLoading } = useAuth();
// Keep splash visible until isLoading === false (see Expo auth docs)
if (isLoading) return null;
return (
<Stack screenOptions={{ headerShown: false }}>
<Stack.Protected guard={!!user}>
<Stack.Screen name="(tabs)" />
</Stack.Protected>
<Stack.Protected guard={!user}>
<Stack.Screen name="(auth)" />
</Stack.Protected>
</Stack>
);
}
Loop prevention: wait for onAuthStateChanged / isLoading; one root gate (Stack.Protected); do not dual-replace between auth and tabs layouts; protected routes are client-side only, not server auth (Expo protected routes).
Errors That Burn Hours (Real Strings + Fixes)
1. auth/invalid-credential
What you see: Firebase: Error (auth/invalid-credential).
Why: With Email Enumeration Protection enabled (default on newer projects), signInWithEmailAndPassword returns this generic code for unknown email or wrong password instead of auth/user-not-found / auth/wrong-password (Firebase Auth JS reference; community confirmation aligns with Firebase's enumeration-protection behavior).
Fix:
- Show a single message: "Email or password is incorrect."
- Do not branch UX on user-not-found vs wrong-password unless you intentionally disable enumeration protection (usually a bad trade).
- Also verify the Email/Password provider is enabled and you are hitting the correct Firebase project.
2. Expo Go limits for Google Sign-In / native auth (footgun)
What you see: missing native modules, Google crashes, or Apple tokens for the wrong bundle ID.
Why: Native Google Sign-In cannot run in Expo Go (Expo Google authentication). Kits using @react-native-firebase/*, google-signin, Apple auth, and expo-dev-client need expo run / EAS.
Fix: Ship a dev client; rebuild after plugins. Treat "works in Expo Go" as a footgun report, not success.
3. Android SHA-1 missing for Google
What you see: Google Sign-In fails on Android (often a developer / configuration style error), while iOS or another build type works.
Why: Android OAuth clients bind package name + SHA-1. Local debug, EAS credentials, upload key, and Play App Signing certificates can all differ (Expo Google guide; Firebase Android troubleshooting).
Fix:
- Collect SHA-1 via
eas credentials -p androidand/or./gradlew signingReport. - Add all relevant fingerprints in Firebase → Project settings → your Android app.
- Re-download
google-services.jsonand rebuild. - For Play Store installs, include the App signing key SHA-1, not only the upload key.
4. Apple Sign In rejects / capability missing
What you see: Apple sheet never appears, entitlement errors, auth/invalid-credential / malformed OAuth after Apple returns, or App Store rejection for missing Apple login.
Why: Missing ios.usesAppleSignIn / entitlement, wrong nonce handling (hashed to Apple, raw to Firebase), Firebase Apple provider misconfigured, or offering Google on iOS without Apple (Expo AppleAuthentication; Firebase Apple).
Fix:
- Enable capability, rebuild the binary (JS-only updates will not add entitlements).
- Fix nonce hashing.
- Confirm Services ID / Team ID / Key in Firebase.
- Keep Apple iOS-only in UI.
- Ship Apple beside other third-party options before review.
5. Redirect / auth state loops with Expo Router
What you see: splash ↔ login ↔ tabs flicker; deep-link bounce; navigation thrash.
Why: Redirect before auth loaded, dual guards fighting, or a second session flag out of band from Firebase.
Fix: Gate on isLoading === false; one AuthProvider + Firebase listener; dual Stack.Protected (user / !user) or one redirect—not both; (auth) ↔ (tabs) via root _layout only—no invented middleware.
Extra footguns worth a checklist row
- Missing files / env:
GoogleService-Info.plist,google-services.json,EXPO_PUBLIC_GOOGLE_WEB_CLIENT_ID—all three must match the Firebase/Google project you think you are calling. - Sensitive account ops: link, unlink, and delete often need recent re-authentication; catch
auth/requires-recent-loginand prompt re-auth. autoGuestMode: leaving this on can skip your auth stack entirely—keep the default false unless that is an explicit product decision.
When a Kit Helps (Soft CTA)
Want email/password, Google (googleSignIn), Apple iOS (appleSignIn), guest (guestMode), emailVerification default OFF, and (auth) / (tabs) gated by AuthProvider already structured? Soft CTA: Expo starter kit—Auth is wired; you still add credentials, config files, and a dev build. Not zero-setup billing.
After login, monetize with RevenueCat paywalls on Expo; keep auth and purchase identity consistent.
FAQ
Does Firebase Auth work in Expo Go?
No for this stack. Google Sign-In, Apple as you ship it, and @react-native-firebase/* need a development build. Expo Go is a footgun for auth QA.
Do I need email verification?
No. Keep emailVerification OFF until templates and continue URLs are ready.
Why does Google work locally but not on Play builds?
Different SHA-1 certificates. Add Play App signing (and upload) fingerprints in Firebase, refresh google-services.json, rebuild.
Is Stack.Protected required?
On SDK 53+ yes, prefer it. Older SDKs: layout redirects. Always wait for auth load, then gate (auth) vs (tabs).
Can guests keep data after signup?
Link the anonymous UID with linkWithCredential. Prompt re-auth when Firebase returns auth/requires-recent-login.
Conclusion
Shipping Firebase Auth on Expo means aligning Firebase providers, native Google/Apple credentials on a dev build, and an Expo Router session gate between (auth) and (tabs). Start with email/password, finish Google SHA-1 coverage, ship Apple on iOS with entitlement + nonce handling, keep guest and email-verification flags intentional, and use the error checklist for auth/invalid-credential, Expo Go limits, and redirect loops.
Prefer Auth already wired? Soft next step: the Expo starter kit—you still own credentials and the native build.
References
- Firebase Auth JS API — signInWithEmailAndPassword / AuthErrorCodes
- Authenticate Using Apple (Firebase)
- Expo — Using Google authentication
- Expo — AppleAuthentication
- Expo Router — Authentication
- Expo Router — Protected routes
- Firebase Android troubleshooting (SHA-1)
- CodeBaseHub Expo starter kit
- How to add RevenueCat paywalls to Expo
Related articles
- What Is an Expo Boilerplate? (And When You Need More Than create-expo-app)Expo Boilerplate vs create-expo-app—what you still have to build (auth, paywalls, push, and store config) plus a checklist before you buy a kit.
- How to Add RevenueCat Paywalls to an Expo App (Without Rebuilding Billing)A practical Expo guide to RevenueCat paywalls—install react-native-purchases, configure entitlements and offerings, presentPaywallIfNeeded, and go live with API keys plus a feature flag. RevenueCat integrated; key + flag to activate, not out-of-box subscriptions.
- From create-expo-app to App Store: What's Still MissingA fresh Expo SDK 57 project ships 23 dependencies, two screens, and zero auth, payments, or push. Here is everything still left to build before you can ship.