← Back to DevBytes

Firebase Auth Best Practices: Cost, Security, and Performance

Introduction to Firebase Authentication

Firebase Authentication (Firebase Auth) is a managed identity service provided by Google that handles user sign-in, registration, and session management for your applications. It supports a wide range of authentication methods including email/password, phone number, social providers (Google, Facebook, Apple, GitHub, etc.), SAML/OIDC enterprise identity, and anonymous auth. Firebase Auth integrates tightly with other Firebase services like Firestore, Realtime Database, and Cloud Functions, making it a popular choice for developers building web, mobile, and backend applications.

At its core, Firebase Auth generates signed JWT tokens that identify users across Firebase services. When a user signs in, the SDK caches the credential locally and manages token refresh automatically. The service handles the complex parts of authentication — password hashing, MFA flows, token lifecycle, and account linking — so you can focus on building your application logic rather than reinventing auth infrastructure.

Why Firebase Auth Best Practices Matter

🚀 Deploy your AI agent in 10 minutes

Managed Hermes hosting. Zero DevOps. 100M tokens/mo included.

Try it free →

Getting Firebase Auth right is critical for three interconnected reasons:

Cost Implications

While Firebase Auth itself is largely free (the base service costs nothing), certain operations trigger billable events. Phone SMS verification, multi-factor authentication with SMS, and identity platform upgrade costs for advanced features like blocking functions or custom claims all contribute to your bill. Poorly designed auth flows — such as repeatedly re-verifying phone numbers, triggering unnecessary token refreshes, or failing to cache credentials — can multiply these costs significantly at scale.

Security Impact

Authentication is your application's front door. A misconfigured Firebase Auth setup can lead to account takeover, unauthorized data access, token theft, or privilege escalation. Common pitfalls include overly permissive security rules that rely solely on authentication checks without proper data validation, exposed API keys leading to abuse on other Firebase services, and failure to implement protections against brute force attacks or token replay.

Performance Consequences

Every authentication check adds latency to your user's experience. Excessive token verification, unnecessary network calls to decode tokens, or blocking UI while waiting for auth state resolution can make your app feel sluggish. On mobile devices, battery and network constraints amplify these issues. Optimizing auth performance directly improves user retention and conversion rates.

Understanding Firebase Auth Pricing and Cost Optimization

What Costs Money

Firebase Auth pricing breaks down as follows:

Cost Optimization Strategies

1. Cache Authentication State Aggressively

One of the most common cost pitfalls is repeatedly calling phone verification or MFA challenges for users who have already been verified. Implement persistent auth state caching so users remain signed in across app restarts:

// Web: Firebase Auth persists auth state by default in IndexedDB
// Ensure you're using the default persistence or LOCAL persistence
import { initializeApp } from 'firebase/app';
import { getAuth, onAuthStateChanged, browserLocalPersistence } from 'firebase/auth';

const app = initializeApp(firebaseConfig);
const auth = getAuth(app);

// Explicitly set persistence to LOCAL for long-lived sessions
await auth.setPersistence(browserLocalPersistence);

// Listen for auth state — this fires once on load with cached user
onAuthStateChanged(auth, (user) => {
  if (user) {
    // User is signed in from cache — no network call needed
    console.log('Cached user:', user.uid);
  } else {
    // No cached user — show sign-in screen
    showSignInUI();
  }
});
// React Native / iOS / Android: Auth state persists automatically
// via platform-specific secure storage (Keychain/Keystore)
import { getAuth, onAuthStateChanged } from '@react-native-firebase/auth';

const auth = getAuth();

// Auth state listener fires immediately with cached credentials
const unsubscribe = onAuthStateChanged(auth, (user) => {
  if (user) {
    // User object available immediately — no phone re-verification needed
    navigateToApp();
  }
});

// Only call verifyPhoneNumber when absolutely necessary
// NOT on every app startup

2. Debounce and Rate-Limit Phone Verification Requests

Phone verification triggers an SMS cost every time it's called. Protect against accidental duplicate sends:

// Implement a cooldown timer for phone verification
let lastVerificationSentAt = 0;
const VERIFICATION_COOLDOWN_MS = 30000; // 30 seconds

async function sendVerificationWithCooldown(phoneNumber, recaptchaVerifier) {
  const now = Date.now();
  if (now - lastVerificationSentAt < VERIFICATION_COOLDOWN_MS) {
    throw new Error('Please wait before requesting another code. Cooldown active.');
  }
  
  lastVerificationSentAt = now;
  const auth = getAuth();
  
  try {
    const confirmationResult = await signInWithPhoneNumber(
      auth, 
      phoneNumber, 
      recaptchaVerifier
    );
    return confirmationResult;
  } catch (error) {
    // Reset cooldown on error so user can retry
    lastVerificationSentAt = 0;
    throw error;
  }
}

3. Use Anonymous Auth for Pre-Registration Experiences

If your app allows users to browse before signing in, use anonymous auth. It's completely free and lets you associate data with a temporary user ID, which upgrades seamlessly when the user signs in with a permanent provider:

// Sign in anonymously — free, no cost
import { getAuth, signInAnonymously, linkWithCredential, 
         EmailAuthProvider } from 'firebase/auth';

const auth = getAuth();

// Create anonymous session
const { user: anonymousUser } = await signInAnonymously(auth);
const anonymousUid = anonymousUser.uid;

// User can write to Firestore with this anonymous UID
// Later, upgrade to permanent account
const credential = EmailAuthProvider.credential(email, password);

try {
  // linkWithCredential merges the anonymous user's data
  // into the new permanent account — no data loss
  const result = await linkWithCredential(
    auth.currentUser, 
    credential
  );
  console.log('Anonymous account upgraded:', result.user.uid);
  // The user retains all data written under the anonymous UID
} catch (error) {
  if (error.code === 'auth/credential-already-in-use') {
    // Handle account merge scenario
    console.log('Credential already associated with another account');
  }
}

4. Avoid Unnecessary Token Refresh Operations

Firebase Auth SDK automatically refreshes tokens when they expire (typically after 1 hour). Forcing manual token refresh via user.getIdToken(true) unnecessarily triggers network calls. Only force refresh when you absolutely need a fresh token with updated claims:

// BAD: Forcing refresh every time you call an API
// This triggers a network call on every invocation
async function badApiCall() {
  const token = await auth.currentUser.getIdToken(true); // forces refresh
  await fetch('/api/data', { headers: { Authorization: `Bearer ${token}` } });
}

// GOOD: Use cached token by default, force refresh only when needed
async function goodApiCall() {
  // getIdToken(false) uses cached token if not expired
  const token = await auth.currentUser.getIdToken(false);
  
  try {
    const response = await fetch('/api/data', { 
      headers: { Authorization: `Bearer ${token}` } 
    });
    
    if (response.status === 401 || response.status === 403) {
      // Only force refresh if token was rejected
      const freshToken = await auth.currentUser.getIdToken(true);
      const retryResponse = await fetch('/api/data', {
        headers: { Authorization: `Bearer ${freshToken}` }
      });
      return retryResponse.json();
    }
    
    return response.json();
  } catch (error) {
    console.error('API call failed:', error);
    throw error;
  }
}

Security Best Practices for Firebase Auth

1. Never Expose Firebase API Keys — But Understand Their Role

Firebase configuration objects contain an API key that is not a secret. This key identifies your project to Google's services but does not grant access on its own. Security comes from your Firestore Security Rules, Firebase Auth configuration, and App Check — not from hiding the API key. However, you should still avoid committing API keys to public repositories if they're tied to services with quota limits. Use environment variables for organizational hygiene:

// .env file (not committed to version control)
VITE_FIREBASE_API_KEY=AIzaSyXXXXXXXXXXXXXXXXXXXX
VITE_FIREBASE_AUTH_DOMAIN=my-project.firebaseapp.com
VITE_FIREBASE_PROJECT_ID=my-project

// config.js — load from environment
import { initializeApp } from 'firebase/app';

const firebaseConfig = {
  apiKey: import.meta.env.VITE_FIREBASE_API_KEY,
  authDomain: import.meta.env.VITE_FIREBASE_AUTH_DOMAIN,
  projectId: import.meta.env.VITE_FIREBASE_PROJECT_ID,
};

export const app = initializeApp(firebaseConfig);

2. Implement App Check for Abuse Protection

App Check verifies that requests to Firebase services come from your genuine app, preventing attackers from using your API key to abuse Firebase Auth endpoints (like brute-forcing phone verification or creating fake accounts). App Check is free and essential:

// Web: Use reCAPTCHA v3 or reCAPTCHA Enterprise
import { initializeApp } from 'firebase/app';
import { initializeAppCheck, ReCaptchaV3Provider } from 'firebase/app-check';

const app = initializeApp(firebaseConfig);

// Initialize App Check with reCAPTCHA
const appCheck = initializeAppCheck(app, {
  provider: new ReCaptchaV3Provider('YOUR_RECAPTCHA_SITE_KEY'),
  isTokenAutoRefreshEnabled: true // tokens auto-refresh
});

// App Check tokens are now attached to all Firebase requests
// This prevents unauthorized use of your Auth endpoints
// iOS: Use DeviceCheck or App Attest
import { initializeApp } from 'firebase/app';
import { initializeAppCheck, DeviceCheckProvider } from 'firebase/app-check';

const app = initializeApp(firebaseConfig);

const appCheck = initializeAppCheck(app, {
  provider: new DeviceCheckProvider(),
  isTokenAutoRefreshEnabled: true
});

3. Enforce Proper Security Rules That Go Beyond "auth != null"

The most common security mistake is writing Firestore rules that only check if a user is authenticated without validating data ownership or structure. This allows any authenticated user to read or write any document:

// DANGEROUS: Allows any authenticated user to read/write everything
rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /{document=**} {
      allow read, write: if request.auth != null;
    }
  }
}

// SECURE: Validate ownership and data structure
rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    // User profiles: only allow users to read/write their own profile
    match /users/{userId} {
      allow read: if request.auth != null;
      allow write: if request.auth != null 
                    && request.auth.uid == userId
                    && request.resource.data.keys().hasOnly(
                      ['name', 'email', 'avatar', 'updatedAt']
                    )
                    && request.resource.data.name is string
                    && request.resource.data.name.size() <= 100;
    }
    
    // Private user data: only the owning user
    match /userPrivateData/{userId}/{document=**} {
      allow read, write: if request.auth != null 
                          && request.auth.uid == userId;
    }
    
    // Public data: anyone authenticated can read, 
    // but only admins can write
    match /publicPosts/{postId} {
      allow read: if request.auth != null;
      allow write: if request.auth != null 
                    && request.auth.token.isAdmin == true;
    }
  }
}

4. Use Custom Claims for Role-Based Access Control

Custom claims embed user roles and permissions directly into the auth token, eliminating the need for a separate lookup query to determine permissions. This is both faster and more secure:

// Setting custom claims via Admin SDK (server-side only)
// Run this in a Cloud Function or secure backend
import { getAuth } from 'firebase-admin/auth';

async function setAdminClaim(userId: string) {
  await getAuth().setCustomUserClaims(userId, {
    isAdmin: true,
    role: 'admin',
    permissions: ['read:all', 'write:all', 'delete:posts']
  });
  
  console.log(`Admin claims set for user: ${userId}`);
}

// Reading claims on the client
import { getAuth, getIdTokenResult } from 'firebase/auth';

const auth = getAuth();
const user = auth.currentUser;

if (user) {
  // Force refresh only once after claim changes to get updated token
  const idTokenResult = await user.getIdTokenResult(true);
  
  console.log('User claims:', idTokenResult.claims);
  
  if (idTokenResult.claims.isAdmin) {
    showAdminPanel();
  }
  
  // Subsequent reads don't need force refresh
  // idTokenResult.claims are available on every token decode
}

5. Protect Against Token Replay and Session Hijacking

Firebase Auth tokens are JWTs that expire after 1 hour by default. While the SDK handles refresh automatically, you should implement additional protections for sensitive operations:

// Require recent authentication for sensitive operations
import { getAuth, reauthenticateWithCredential, 
         EmailAuthProvider } from 'firebase/auth';

async function performSensitiveOperation() {
  const auth = getAuth();
  const user = auth.currentUser;
  
  if (!user) throw new Error('No user signed in');
  
  // Check if user authenticated recently (within last 5 minutes)
  const lastSignInTime = user.metadata.lastSignInTime;
  const fiveMinutesAgo = new Date(Date.now() - 5 * 60 * 1000);
  
  if (new Date(lastSignInTime) < fiveMinutesAgo) {
    // Require re-authentication for sensitive operations
    const password = promptForPassword();
    const credential = EmailAuthProvider.credential(user.email, password);
    
    try {
      await reauthenticateWithCredential(user, credential);
      console.log('User re-authenticated successfully');
    } catch (error) {
      console.error('Re-authentication failed:', error);
      throw new Error('Could not verify identity');
    }
  }
  
  // Now proceed with the sensitive operation
  await deleteUserAccount();
}

6. Configure Authorized Domains Properly

Firebase Auth restricts sign-in redirects to domains you've authorized in the Firebase console. Keep this list minimal to prevent phishing attacks that could intercept OAuth redirects:

// Only these domains are authorized for OAuth redirects
// Configure in Firebase Console → Authentication → Settings → Authorized Domains

// Development: localhost, 127.0.0.1
// Production: myapp.com, api.myapp.com
// NEVER include: wildcards (*) or third-party domains

// Code example: verifying redirect domain in Cloud Functions
import { onCall, HttpsError } from 'firebase-functions/v2/https';

export const secureOperation = onCall(async (request) => {
  // Check the origin of the request
  const origin = request.rawRequest?.headers?.origin;
  const allowedOrigins = [
    'https://myapp.com',
    'https://admin.myapp.com'
  ];
  
  if (origin && !allowedOrigins.includes(origin)) {
    throw new HttpsError(
      'permission-denied', 
      'Requests from this origin are not allowed'
    );
  }
  
  // Proceed with the operation
  return { success: true };
});

Performance Optimization for Firebase Auth

1. Initialize Auth Early and Listen to State Reactively

The most impactful performance optimization is resolving auth state quickly on app startup. The Firebase SDK caches credentials locally, but you must set up your listener before rendering to avoid a flash of the wrong UI:

// Web: Set up auth state listener BEFORE rendering your app
import { initializeApp } from 'firebase/app';
import { getAuth, onAuthStateChanged } from 'firebase/auth';

const app = initializeApp(firebaseConfig);
const auth = getAuth(app);

// Create a promise that resolves when auth state is known
function getAuthReadyPromise() {
  return new Promise((resolve) => {
    const unsubscribe = onAuthStateChanged(auth, (user) => {
      unsubscribe(); // Only need the initial state
      resolve(user);
    });
  });
}

// Bootstrap your app only after auth state is resolved
async function bootstrap() {
  showLoadingScreen();
  
  const user = await getAuthReadyPromise();
  
  if (user) {
    // User is signed in — render authenticated app immediately
    renderAuthenticatedApp(user);
  } else {
    // No user — render public-facing app
    renderPublicApp();
  }
  
  hideLoadingScreen();
  
  // THEN set up a persistent listener for future changes
  onAuthStateChanged(auth, (user) => {
    // Handle sign-out or sign-in changes after initial load
    if (!user) {
      navigateToLogin();
    }
  });
}

bootstrap();

2. Memoize Token Retrieval to Reduce Network Overhead

When calling backend APIs that require authentication, cache the token in memory and only fetch a new one when it's about to expire:

// Token caching utility
class TokenManager {
  private cachedToken: string | null = null;
  private tokenExpiryTime: number = 0;
  private refreshPromise: Promise | null = null;
  
  async getToken(forceRefresh = false): Promise {
    const now = Date.now();
    const BUFFER_MS = 60_000; // Refresh 1 minute before expiry
    
    // Return cached token if it's still valid
    if (!forceRefresh && 
        this.cachedToken && 
        now < this.tokenExpiryTime - BUFFER_MS) {
      return this.cachedToken;
    }
    
    // If a refresh is already in progress, wait for it
    if (this.refreshPromise) {
      return this.refreshPromise;
    }
    
    // Fetch a new token
    this.refreshPromise = this.fetchNewToken();
    const token = await this.refreshPromise;
    this.refreshPromise = null;
    return token;
  }
  
  private async fetchNewToken(): Promise {
    const auth = getAuth();
    const user = auth.currentUser;
    
    if (!user) throw new Error('No authenticated user');
    
    // Get token and decode expiry
    const token = await user.getIdToken(!!this.cachedToken === false);
    const decoded = JSON.parse(atob(token.split('.')[1]));
    
    this.cachedToken = token;
    this.tokenExpiryTime = decoded.exp * 1000; // Convert to milliseconds
    
    console.log(`Token cached, expires at: ${new Date(this.tokenExpiryTime)}`);
    return token;
  }
  
  clearCache() {
    this.cachedToken = null;
    this.tokenExpiryTime = 0;
  }
}

const tokenManager = new TokenManager();

// Use in API calls
async function authenticatedFetch(url: string) {
  const token = await tokenManager.getToken();
  
  return fetch(url, {
    headers: {
      Authorization: `Bearer ${token}`,
      'Content-Type': 'application/json'
    }
  });
}

3. Avoid Blocking UI with Auth Checks — Use Optimistic Rendering

For many screens, you can render the UI optimistically while auth state is resolving. Use skeleton screens and progressive enhancement instead of a blank loading screen:

// React example: Optimistic rendering with auth state
import { getAuth, onAuthStateChanged } from 'firebase/auth';
import { useState, useEffect } from 'react';

function App() {
  const [user, setUser] = useState(null);
  const [authLoading, setAuthLoading] = useState(true);
  
  useEffect(() => {
    const auth = getAuth();
    
    // Show skeleton UI immediately while auth resolves
    const unsubscribe = onAuthStateChanged(auth, (currentUser) => {
      setUser(currentUser);
      setAuthLoading(false);
    });
    
    return () => unsubscribe();
  }, []);
  
  // Render UI progressively — never show blank screen
  return (
    
{/* Navigation renders immediately */} {/* Main content area */} {authLoading ? ( // Show skeleton, not blank screen ) : user ? ( ) : ( )}
); }

4. Batch Auth Operations to Reduce Round Trips

When you need to perform multiple auth-dependent operations, batch them into a single Cloud Function call rather than making multiple client-side requests:

// Cloud Function: Batch multiple operations into one call
import { onCall } from 'firebase-functions/v2/https';
import { getAuth } from 'firebase-admin/auth';
import { getFirestore } from 'firebase-admin/firestore';

export const initializeUserAccount = onCall(async (request) => {
  const uid = request.auth?.uid;
  if (!uid) throw new Error('Authentication required');
  
  // Run multiple operations in parallel server-side
  const [userRecord, userDoc] = await Promise.all([
    // Get full user profile from Auth
    getAuth().getUser(uid),
    // Fetch user preferences from Firestore
    getFirestore().collection('userPreferences').doc(uid).get()
  ]);
  
  // Return aggregated result in one response
  return {
    profile: {
      email: userRecord.email,
      emailVerified: userRecord.emailVerified,
      createdAt: userRecord.metadata.creationTime,
      claims: userRecord.customClaims
    },
    preferences: userDoc.exists ? userDoc.data() : null
  };
});

// Client: Single call instead of multiple requests
import { getFunctions, httpsCallable } from 'firebase/functions';

async function loadUserDashboard() {
  const functions = getFunctions();
  const initializeAccount = httpsCallable(functions, 'initializeUserAccount');
  
  // One network call gets everything
  const result = await initializeAccount();
  console.log('Profile:', result.data.profile);
  console.log('Preferences:', result.data.preferences);
  
  return result.data;
}

5. Use Link Providers Efficiently on Mobile

On mobile platforms, social provider sign-in often requires opening external apps or browsers. Use the native provider SDKs for a faster, more reliable experience:

// iOS: Use native Google Sign-In SDK for faster auth
import { GoogleSignIn } from '@react-native-google-signin/google-signin';
import { getAuth, GoogleAuthProvider, signInWithCredential } from '@react-native-firebase/auth';

async function signInWithGoogleNative() {
  // Configure Google Sign-In once at app startup
  await GoogleSignIn.configure({
    webClientId: 'YOUR_WEB_CLIENT_ID', // From Firebase console
    offlineAccess: false,
    hostedDomain: '',
  });
  
  // Native Google Sign-In prompt — no web browser needed
  const signInResult = await GoogleSignIn.signIn();
  
  // Get the ID token from native SDK
  const { idToken } = await GoogleSignIn.signIn();
  
  // Create Firebase credential from native token
  const googleCredential = GoogleAuthProvider.credential(idToken);
  
  // Sign in to Firebase — single fast operation
  const auth = getAuth();
  const userCredential = await signInWithCredential(auth, googleCredential);
  
  console.log('Signed in with Google:', userCredential.user.uid);
  return userCredential.user;
}

Implementation Examples and Code Patterns

Complete Auth Flow with Error Handling

Here's a production-ready implementation combining cost optimization, security, and performance practices:

// auth-service.ts — Centralized auth service
import { initializeApp } from 'firebase/app';
import {
  getAuth,
  onAuthStateChanged,
  signInWithEmailAndPassword,
  createUserWithEmailAndPassword,
  signOut,
  sendPasswordResetEmail,
  browserLocalPersistence,
  setPersistence,
  User,
  AuthError
} from 'firebase/auth';
import { getFirestore, doc, getDoc } from 'firebase/firestore';

// Initialize Firebase
const app = initializeApp({
  apiKey: import.meta.env.VITE_FIREBASE_API_KEY,
  authDomain: import.meta.env.VITE_FIREBASE_AUTH_DOMAIN,
  projectId: import.meta.env.VITE_FIREBASE_PROJECT_ID,
});

const auth = getAuth(app);
const db = getFirestore(app);

// Token cache
let tokenCache: { token: string; expiresAt: number } | null = null;

class AuthService {
  private authReadyPromise: Promise;
  private currentUser: User | null = null;
  
  constructor() {
    // Set persistence once at startup
    this.authReadyPromise = this.initializeAuth();
  }
  
  private async initializeAuth(): Promise {
    // Enable long-lived sessions (survives browser restarts)
    await setPersistence(auth, browserLocalPersistence);
    
    // Wait for initial auth state resolution
    return new Promise((resolve) => {
      const unsubscribe = onAuthStateChanged(auth, (user) => {
        this.currentUser = user;
        unsubscribe();
        resolve(user);
      });
    });
  }
  
  async waitForAuthReady(): Promise {
    return this.authReadyPromise;
  }
  
  async signIn(email: string, password: string): Promise {
    try {
      const credential = await signInWithEmailAndPassword(auth, email, password);
      this.currentUser = credential.user;
      tokenCache = null; // Clear token cache on new sign-in
      return credential.user;
    } catch (error) {
      const authError = error as AuthError;
      console.error('Sign-in error code:', authError.code);
      
      // Handle specific errors with user-friendly messages
      switch (authError.code) {
        case 'auth/user-not-found':
          throw new Error('No account found with this email address');
        case 'auth/wrong-password':
          throw new Error('Invalid password. Please try again');
        case 'auth/invalid-email':
          throw new Error('Please enter a valid email address');
        case 'auth/too-many-requests':
          throw new Error('Too many attempts. Please try again later');
        case 'auth/user-disabled':
          throw new Error('This account has been disabled');
        default:
          throw new Error('Sign-in failed. Please try again');
      }
    }
  }
  
  async register(email: string, password: string): Promise {
    try {
      const credential = await createUserWithEmailAndPassword(auth, email, password);
      this.currentUser = credential.user;
      tokenCache = null;
      return credential.user;
    } catch (error) {
      const authError = error as AuthError;
      
      switch (authError.code) {
        case 'auth/email-already-in-use':
          throw new Error('An account with this email already exists');
        case 'auth/weak-password':
          throw new Error('Password must be at least 6 characters');
        case 'auth/invalid-email':
          throw new Error('Please enter a valid email address');
        default:
          throw new Error('Registration failed. Please try again');
      }
    }
  }
  
  async getToken(forceRefresh = false): Promise {
    if (!this.currentUser) {
      throw new Error('No authenticated user');
    }
    
    // Check memory cache first
    const now = Date.now();
    if (!forceRefresh && tokenCache && now < tokenCache.expiresAt - 60_000) {
      return tokenCache.token;
    }
    
    // Fetch fresh token
    const token = await this.currentUser.getIdToken(forceRefresh);
    
    // Decode expiry and cache
    const payload = JSON.parse(atob(token.split('.')[1]));
    tokenCache = {
      token,
      expiresAt: payload.exp * 1000
    };
    
    return token;
  }
  
  async getUserProfile(): Promise {
    if (!this.currentUser) throw new Error('No authenticated user');
    
    // Fetch profile from Firestore — single document read
    const userDoc = await getDoc(doc(db, 'users', this.currentUser.uid));
    
    if (!userDoc.exists()) {
      return {
        uid: this.currentUser.uid,
        email: this.currentUser.email,
        emailVerified: this.currentUser.emailVerified,
        createdAt: this.currentUser.metadata.creationTime
      };
    }
    
    return {
      uid: this.currentUser.uid,
      email: this.currentUser.email,
      ...userDoc.data()
    };
  }
  
  async resetPassword(email: string): Promise {
    try {
      await sendPasswordResetEmail(auth, email);
    } catch (error) {
      const authError = error as AuthError;
      if (authError.code === 'auth/user-not-found') {
        // Don't reveal whether the email exists
        console.log('Password reset requested for non-existent email');
        return; // Still return success to prevent user enumeration
      }
      throw new Error('Failed to send reset email. Please try again');
    }
  }
  
  async logout(): Promise {
    tokenCache = null;
    this.currentUser = null;
    await signOut(auth);
  }
  
  onAuthChange(callback: (user: User | null) => void): () => void {
    return onAuthStateChanged(auth, (user) => {
      this.currentUser = user;
      if (!user) tokenCache = null;
      callback(user);
    });
  }
  
  getCurrentUser(): User | null {
    return this.currentUser;
  }
}

// Export singleton
export const authService = new AuthService();

Secure Backend Token Verification

Always verify Firebase Auth tokens on your backend. Never trust client-submitted UIDs without verification:

// Node.js backend: Verify Firebase Auth tokens
import { initializeApp, cert } from 'firebase-admin/app';
import { getAuth } from 'firebase-admin/auth';

// Initialize Admin SDK with service account
initializeApp({
  credential: cert('./service-account-key.json'),
});

async function verifyRequest(authHeader: string) {
  if (!authHeader || !authHeader.startsWith('Bearer ')) {
    throw new Error('Missing or invalid Authorization header');
  }
  
  const token = authHeader.split('Bearer ')[1];
  
  try {
    // Verify the token and extract user info
    const decodedToken = await getAuth().verifyIdToken(token, true);
    // The second parameter 'true' checks for token revocation
    
    console.log('Verified user:', decodedToken.uid);
    console.log('Email:', decodedToken.email);
    console.log('Custom claims:', decodedToken.admin);
    
    return {
      uid: decodedToken.uid,
      email: decodedToken.email,
      emailVerified: decodedToken.email_verified,
      claims: decodedToken,
      authTime: new Date(decodedToken.auth_time * 1000)
    };
  } catch (error) {
    console.error('Token verification failed:', error);
    throw new Error('Invalid or expired authentication token');
  }
}

// Express middleware example
import express from 'express';

const app = express();

async function authMiddleware(req, res, next) {
  try {
    const authHeader = req.headers.authorization;
    const userInfo = await verifyRequest(authHeader);
    req.user = userInfo;
    next();
  } catch (error) {
    res.status(401).json({ error: 'Authentication required' });
  }
}

// Protected route
app.get('/api/protected-data', authMiddleware, async (req, res) => {
  // req.user is verified and safe to use
  const userData = await getDataForUser(req.user.uid);
  res.json(userData);
});

Handling Token Revocation and Session Management

For high-security applications, implement token revocation checking to immediately invalidate compromised sessions:

// Revoke refresh tokens for compromised sessions
import { getAuth } from 'firebase-admin/auth';

async function revokeUserSessions(userId: string) {
  try {
    // Revoke all refresh tokens for the user
    // This invalidates all existing sessions immediately
    await getAuth().revokeRefreshTokens(userId);
    
    console.log(`All sessions revoked for user: ${userId}`);
    
    // Optionally, force the user to re-authenticate
    // by clearing custom claims or flagging the account
    await getAuth().setCustomUserClaims(userId, {
      ...existingClaims,
      requiresReauth: true,
      sessionRevokedAt: Date.now()
    });
    
    return { success: true };
  } catch (error) {
    console.error('Failed to revoke sessions:', error);
    throw error;
  }
}

// Client-side: Handle revoked tokens gracefully
import { getAuth, onAuthStateChanged, signOut } from 'firebase/auth';

const auth = getAuth();

onAuthStateChanged(auth, async (user) => {
  if (user) {
    try {
      // Check if

🚀 Need a reliable AI agent for your project?

Deploy Hermes Agent in 10 minutes. Managed hosting, zero DevOps.

Get Started — $23.99/mo
← Back to all articles