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:
- Free tier (Spark plan): Unlimited sign-in events for email/password and social providers. 10K/month free for phone/SMS verification
- Blaze plan (pay-as-you-go): Phone verification above 10K/month costs $0.01 per verification in most regions (US, Canada, India) and $0.03–$0.06 elsewhere
- Multi-factor authentication (SMS): Same pricing as phone verification — $0.01–$0.06 per MFA challenge
- Identity Platform upgrade: Advanced features like blocking functions, multi-tenancy, and custom claims with SAML/OIDC require upgrading to Identity Platform, which has per-MAU pricing starting around $0.0055/MAU for the first 10K users
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