Skip to main content

How to Test Firebase Authentication Flows

Short answer

Firebase Auth spans client SDK state, ID tokens, Cloud Functions, Firestore profiles, and security rules—UI-green login is not proof of a valid session. Use the Auth emulator (or a dedicated test project), per-run users via Admin SDK or custom tokens, and probe Assert on APIs and Firestore—not shared test@company.com accounts in parallel CI.

Part of Testing Guides by auth and identity.

Who this is for

Teams shipping Firebase Authentication on web (often with Firestore, Cloud Functions, and Hosting) who need Playwright E2E that survives parallel CI, provider additions, and verification gates—not one-off scripts that log in once against staging.

Typical stacks: Next.js + Firebase SDK, Vite/React + Firebase, Flutter web with Firebase, or Lovable/Supabase-adjacent apps that added Firebase Auth later.

Why testing Firebase Auth matters

Auth bugs are rarely cosmetic. They show up as:

  • Revenue loss — paywalled features reachable without a valid ID token; trial users retain access after subscription lapse because client state says "logged in" while custom claims were revoked.
  • Security incidents — cross-tenant data exposure when Firestore rules trust client-written tenantId; disabled users still holding refresh tokens; OAuth account linking creating duplicate identities.
  • Support load — email verification loops, password reset links that expire silently, phone OTP rate limits blocking legitimate users in prod while tests never covered resend cooldown.
  • Compliance exposure — GDPR/CCPA delete-account flows that remove Auth user but leave PII in Firestore; audit logs missing for admin impersonation.

Firebase makes it easy to appear logged in on the client (onAuthStateChanged fires) while your backend rejects requests— or the opposite: API accepts stale tokens because verify middleware is misconfigured. E2E must assert the full boundary: Auth user record, ID token claims, Firestore profile, and protected HTTP routes.

Complexity map

ScenarioEdge caseWhy tests breakApproach
Emulator vs cloud projectIdentity Toolkit rate limits on signupRandom 429 in CIAuth emulator for automated runs
connectAuthEmulator orderCalled after first Auth opSilent connection to prodInit emulator before app bootstrap in tests
Email verification gateProtected route checks emailVerifiedCannot click real inbox in CIAdmin SDK toggle or transactional email guide
Password resetOOB code single-use, expiryLink consumed by bot prefetchExtract oobCode param; call confirm API directly
Wrong password / lockoutGeneric Firebase error codesAsserting toast copy flakesProbe session null + auth/wrong-password if exposed
Custom token loginService account in CI secretsSkipped entirelySeed route mints token per runId
Custom claims refreshClaims updated server-sideClient token stale up to 1hForce token refresh or re-login in test
Google / Apple OAuthPopup blocked headlessHangs foreverSplit: emulator fake provider OR API Arrange for post-login tests
Anonymous → permanentLink credential collision"Account exists" edgeSeed anonymous user; link with fixture email
Phone authReal SMS costUntested in CIEmulator test numbers + fixed codes
Firestore profileonCreate function lagProbe finds no /users/{uid}expect.poll on probe
Security rulesClient set() appears to work locallyFalse confidenceProbe via Admin SDK; rules unit tests separately
Multi-tenant (Identity Platform)Wrong tenant ID in tokenCross-tenant accessSeed per-tenant user; probe tenant claim
Disabled userdisabled: true on recordError message variesProbe sign-in failure + no session cookie
Token refresh / expiryLong test suiteMid-test 401Short-lived test tokens or refresh helper
Account deletionAuth deleted, Firestore orphanedGDPR failureProbe both Auth and Firestore absence

Emulator vs dedicated test project

Auth emulatorFirebase test project (cloud)
CI costFree, localQuota + billing alerts
Rate limitsNone for auth opsIdentity Toolkit limits apply
OAuth providersFake/simplifiedReal Google/Apple redirects
Phone SMSFixed test codesTest phone numbers configured in console
Parallel workersExcellent (isolated)Risky if sharing users
Production parityRules/emulator quirksClosest to prod behavior

Recommendation: Default all Playwright CI to the Auth + Firestore emulators. Keep a small nightly or pre-release job against a dedicated test Firebase project only for flows that truly require real OAuth redirects—document which scenarios need that job so they do not block every PR.

Start emulators in CI

# firebase.json should declare emulators.auth.port (default 9099) and firestore
firebase emulators:start --only auth,firestore --project demo-test &
EMULATOR_PID=$!

# Wait for ports — do not start Playwright before this
npx wait-on tcp:9099 tcp:8080

npm run test:e2e

kill $EMULATOR_PID

Set for Admin SDK and server-side seed routes:

export FIREBASE_AUTH_EMULATOR_HOST=127.0.0.1:9099
export FIRESTORE_EMULATOR_HOST=127.0.0.1:8080

Client SDK (must run before any auth method):

import { connectAuthEmulator, getAuth } from 'firebase/auth';

const auth = getAuth();
if (process.env.NEXT_PUBLIC_USE_AUTH_EMULATOR === 'true') {
connectAuthEmulator(auth, 'http://127.0.0.1:9099', { disableWarnings: true });
}

Import order matters: if your app calls signInAnonymously on module load before connectAuthEmulator, tests hit production Auth silently.

Arrange: users without UI signup

Most E2E tests are about features after login, not the login form itself. Creating users through the signup UI for every spec is slow, couples tests to CSS changes, and collides in parallel CI.

Pattern A — Admin SDK in seed route (preferred)

Expose a test-only HTTP route (guarded by env flag) that your Playwright tests call in Arrange:

// POST /api/test/seed-user
// Body: { runId: string, emailVerified?: boolean, claims?: object }
// Response: { customToken: string, uid: string }

import { getAuth } from 'firebase-admin/auth';

const email = `e2e-${runId}@test.local`;
const user = await getAuth().createUser({
email,
password: `pw-${runId}`,
emailVerified: emailVerified ?? true,
});
if (claims) {
await getAuth().setCustomUserClaims(user.uid, claims);
}
const customToken = await getAuth().createCustomToken(user.uid, claims);
return { customToken, uid: user.uid };

Playwright Arrange:

const runId = test.info().parallelIndex + '-' + Date.now();
const { customToken } = await request.post('/api/test/seed-user', {
data: { runId, emailVerified: true },
}).then(r => r.json());

await page.goto('/test/sign-in-with-token'); // app page that calls signInWithCustomToken
await page.evaluate(async (token) => {
const { getAuth, signInWithCustomToken } = await import('firebase/auth');
await signInWithCustomToken(getAuth(), token);
}, customToken);

Next.js and other SSR frameworks often use session cookies minted server-side (firebase-admin createSessionCookie). Seed route creates user + sets HttpOnly cookie—Playwright context.addCookies() or one navigation to /api/test/login?runId= avoids client SDK timing issues entirely.

Pattern C — UI signup only when testing signup

Reserve UI signup specs for:

  • Registration form validation
  • Password strength policy messaging
  • Terms acceptance checkbox
  • Marketing attribution on signup

Everything else: Pattern A or B.

Email and password flows

FlowWhat goes wrong in prodArrangeActAssert
Login successSession not propagated to SSRSeed verified userUI login OR custom tokenProbe /api/me returns uid; Firestore profile exists
Wrong passwordBrute-force lockout untestedSeed userBad passwordProbe no session; optional auth/wrong-password
Unverified email gateDashboard loads, API 403emailVerified: falseHit protected routeProbe 401/403 until Admin verifies
Weak password rejectedWeak pw storedNoneSubmit short passwordProbe user not created
Email already in useDuplicate accountsSeed existing emailRegister againProbe single user row; UI error optional
Password reset happy pathLink expired in emailSeed userRequest resetMailtrap inbox → extract link (email guide)
Password reset expired codeUser stuckSeed expired OOB codeSubmit new passwordProbe failure; no password change
Change password re-authRecent login requiredSeed user, stale sessionChange passwordProbe requires re-auth flow

Firebase returns stable error codes (auth/user-not-found, auth/wrong-password)—prefer probing API behavior over asserting exact error string copy, which changes with i18n.

Email verification

Three legitimate approaches—pick by what the test actually covers:

  1. Admin SDK (fastest for gates) — create user with emailVerified: false, confirm protected API returns 403, call updateUser({ emailVerified: true }), confirm 200. Does not test email template or link format.

  2. Disposable inbox (full path) — use Mailtrap/Mailosaur to capture Firebase verification email, extract link, page.goto(link). Tests template + routing. See transactional email testing.

  3. Emulator OOB helpers — emulator exposes verification codes in some flows; check current Firebase emulator docs for OOB retrieval APIs.

Bot prefetch pitfall: some email scanners GET verification links before the user does, consuming single-use codes. Mitigations: use emulator Admin toggle for CI; for inbox tests, extract oobCode query param and POST to Firebase's verify endpoint instead of full link navigation (same pattern as Stytch E2E guidance).

Custom tokens and custom claims

Custom claims drive authorization in many Firebase apps (role, tenantId, plan). Claims embed in the ID token but refresh asynchronously on the client after setCustomUserClaims.

await admin.auth().setCustomUserClaims(uid, {
role: 'editor',
tenantId: runId,
plan: 'pro',
});

After updating claims in Arrange, force refresh in the browser before Act:

await page.evaluate(async () => {
const auth = (await import('firebase/auth')).getAuth();
await auth.currentUser?.getIdToken(true);
});

E2E negative test: user with role: 'viewer' must receive 403 from admin API even if UI hides buttons—probe the API, not just hidden DOM.

For multi-tenant Identity Platform, pass tenantId when creating users and minting tokens; assert probes cannot read another tenant's Firestore path.

Google, Apple, and social sign-in

Real OAuth in headless Linux CI is painful (popups, 2FA on test Google accounts, Apple JS SDK constraints). Use a tiered strategy:

TierCoverageWhen
Post-login E2ECustom token as Google-linked userEvery PR — features after OAuth
Emulator / fake providerButton → emulator stub userPR if emulator supports provider
Headed / manual / nightlyFull Google popupWeekly or pre-release
TrueCoverageProd auth_provider=google.com sharePrioritize tier-2/3 if slice is large

Linking flows (same email, password account + Google) need explicit scenarios: probe one Firebase uid after link; attempt duplicate link and expect auth/account-exists-with-different-credential.

See also OAuth social login guide.

Anonymous authentication

Anonymous auth is easy to overlook but common in demos and mobile-first web apps.

Test:

  • Anonymous user can access allowed routes
  • Linking email/password or Google promotes the same uid (probe uid unchanged, isAnonymous false)
  • Reinstall / cleared storage creates new anonymous user—no data bleed (probe old uid inaccessible)
  • Server rejects anonymous token on paid APIs (probe 403)

Phone authentication

Never send real SMS in CI. With the Auth emulator, use documented test phone numbers—verification codes are fixed (commonly 123456 for +1 650-555-3434 in emulator docs; verify against your emulator version).

Cover:

  • Valid code → session established, probe profile created
  • Invalid code → no session
  • Resend cooldown — rapid resend should fail (may need clock fixture)
  • Phone number already linked to another account

For cloud test projects, configure test phone numbers in Firebase Console with fixed codes—still avoid real numbers.

Firestore profile sync and Cloud Functions

A typical pattern:

Auth onCreate → Cloud Function → creates /users/{uid}

Tests that probe Firestore immediately after signup flake when the function is cold or slow.

await expect.poll(
async () => {
const res = await request.get(`/api/test/probe-user/${uid}`);
return (await res.json()).displayName;
},
{ timeout: 15_000 },
).toBe('Expected Name');

Also test failure paths: function throws → user sees error state, Auth user rolled back or marked incomplete (probe both sides). If you use emulator functions, wire Functions emulator too—Auth-only emulator with prod Functions causes confusing partial behavior.

Security rules and Auth together

Client SDK writes can appear to succeed in dev with loose rules. Minimum E2E bar:

  • User A cannot read /users/{userB_uid}/private (probe via client SDK in test or API returning rules evaluation)
  • Unauthenticated request to protected collection fails
  • Custom claim required for admin collection—token without claim fails

Run @firebase/rules-unit-testing in CI for exhaustive rules matrices; keep one E2E per critical isolation boundary as integration proof.

Disabled accounts, deletion, and session revocation

ActionAssert
Admin disables userSign-in fails; existing refresh tokens rejected on next API call
User deletes accountAuth user gone; Firestore PII removed or anonymized (probe)
Password changed elsewhereOld session invalidated if you implement revocation
signOutCookie cleared; probe /api/me 401

Deletion flows are GDPR-relevant—probe downstream systems (Storage objects, Algolia index row), not only deleteUser().

CI checklist

  1. connectAuthEmulator before app init in test env
  2. FIREBASE_AUTH_EMULATOR_HOST set for Admin SDK seed routes
  3. Unique email per worker: e2e-${parallelIndex}-${runId}@test.local
  4. No production Firebase config in CI secrets for default PR job
  5. Seed routes disabled when NODE_ENV=production
  6. Global teardown removes emulator users if reusing long-lived emulator (usually ephemeral CI VMs skip this)
  7. Playwright storageState per test when isolating sessions—do not reuse logged-in state across specs with different roles

Anti-patterns

Anti-patternWhy it failsBetter approach
Shared test@company.comParallel workers overwrite password/sessionPer-run email via seed
UI login in every spec5–15s overhead; form churnCustom token Arrange
Assert Firebase UI error texti18n and copy changesProbe session + error code
Only client onAuthStateChangedSSR/API may disagreeProbe HTTP with Bearer token
Skip unverified-email pathShip paywall bypassNegative scenario with Admin toggle
Test against prod FirebaseQuota, PII, lockoutEmulator default
waitForTimeout(3000) after signupFunction still coldexpect.poll probe
Ignore custom claim refreshFlaky role testsgetIdToken(true) after claims update

Example scenario

Situation: An unverified user signs in and navigates to /dashboard.

Expected outcome: Access denied until email is verified—no partial data leak.

Why UI-only automation breaks: Dashboard shell renders but API returns empty arrays—test passes on skeleton UI.

  1. Arrange: Auth emulator + Admin SDK create user with emailVerified: false for runId.
  2. Act: Sign in via custom token or UI, then goto /dashboard.
  3. Assert: Probe /api/me or /api/dashboard returns 403; after Admin sets emailVerified: true and token refresh, probe returns 200 with user data.

TestChimp workflow: Instrument sign_in events with auth_provider and verification_state; compare prod vs test runs in TrueCoverage.

Same Arrange/Act/Assert pattern as expired-coupon checkout.

Connect scenarios to your QA workflow

Capture business rules in markdown test plans and enforce them with seed routes and probe Assert. Link SmartTests with // @Scenario: for requirement traceability. Use /testchimp test on PRs; /testchimp explore on SmartTest paths for non-functional gaps (ExploreChimp).

External references

Frequently asked questions

Should I use the Firebase Auth emulator or a test project for E2E?

Use the Auth emulator for every PR—no rate limits, parallel-safe users, no PII in Google cloud. Reserve a dedicated cloud test project for nightly jobs that must exercise real Google or Apple OAuth redirects; document which scenarios require that job.

How do I log in without the signup UI on every test?

Add a test-only seed route that creates a user with Admin SDK and returns a custom token or session cookie scoped to runId. Playwright Arrange calls signInWithCustomToken or sets cookie, then tests focus on post-login behavior.

How do I test email verification without clicking emails in CI?

Fast path: create user with emailVerified false, probe protected API 403, Admin updateUser to verified true, refresh token, probe 200. Full path: capture Firebase verification email via Mailtrap and navigate link—or extract oobCode to avoid bot prefetch consuming the link.

Why do my tests pass login but fail on API calls?

Client onAuthStateChanged does not guarantee SSR or REST middleware sees a valid ID token. Probe your API with the same session cookie or Authorization header the app uses. After setCustomUserClaims, call getIdToken(true) before acting as that role.

How do I test Firestore user profiles created by Cloud Functions?

Poll a probe endpoint or Firestore read with expect.poll and 10–15s timeout—never fixed sleep. If functions run in cloud while Auth uses emulator, align emulators or probe via Admin SDK consistently.

Can I run Firebase Auth E2E in parallel Playwright workers?

Yes with emulator + unique email per worker (e2e-{parallelIndex}-{timestamp}@test.local). Avoid shared credentials. storageState fixtures should not reuse sessions across specs with different roles or tenants.

We support email, Google, and phone sign-in—how do we know each is covered?

Compare auth_provider and verification_state distributions in prod vs test runs via TrueCoverage. If Google dominates prod but tests only cover password, prioritize OAuth-tier tests or nightly headed runs for that slice—track scenarios in markdown with // @Scenario: links.

Firebase + Supabase—which auth guide applies?

Firebase-first stacks use this guide; Supabase Auth + RLS uses [Supabase E2E](/guides/integrations/testing-supabase-playwright). Both need per-run users—see [shared auth gotcha](/guides/gotchas/e2e-shared-auth-state-pollution).

Should every spec call the login UI?

No—UI login in every test is slow and flaky. Use custom token or cookie Arrange; reserve one spec for login UX. See [login every test gotcha](/guides/gotchas/playwright-login-every-test-slow-flaky).

Apply these patterns in your repo

Run `/testchimp init` to connect TestChimp to your repo, then `/testchimp test` on PRs to turn these patterns into maintained SmartTests. Use `/testchimp evolve` when you want to expand coverage as your app grows.

Start free on TestChimp · Book a demo