[{"data":1,"prerenderedAt":505},["ShallowReactive",2],{"blog-how-to/firebase-auth":3},{"id":4,"title":5,"body":6,"category":486,"date":487,"dateModified":487,"description":488,"draft":489,"extension":490,"faq":491,"featured":489,"headerVariant":492,"image":491,"keywords":491,"meta":493,"navigation":494,"ogDescription":495,"ogTitle":491,"path":496,"readTime":491,"schemaOrg":497,"schemaType":498,"seo":499,"sitemap":500,"stem":501,"tags":502,"twitterCard":503,"__hash__":504},"blog/blog/how-to/firebase-auth.md","How to Set Up Firebase Auth Securely",{"type":7,"value":8,"toc":469},"minimark",[9,13,17,21,27,32,60,69,73,111,156,169,182,211,227,245,258,263,296,300,341,345,349,352,356,359,363,366,370,377,381,388,428,450],[10,11],"category-badge",{"category":12},"How-To Guide",[14,15,5],"h1",{"id":16},"how-to-set-up-firebase-auth-securely",[18,19,20],"p",{},"Complete authentication with security rules and custom claims",[22,23,24],"tldr",{},[18,25,26],{},"TL;DR (20 minutes):\nInitialize Firebase Auth, enable only needed providers, always use\nrequest.auth\nin security rules, verify ID tokens server-side with Admin SDK for sensitive operations, use custom claims for roles (not client-set data), and never trust client-side auth state for critical decisions.",[28,29,31],"h2",{"id":30},"prerequisites","Prerequisites",[33,34,35,47,50,57],"ul",{},[36,37,38,39,46],"li",{},"A Firebase project (create one at ",[40,41,45],"a",{"href":42,"rel":43},"https://console.firebase.google.com",[44],"nofollow","console.firebase.google.com",")",[36,48,49],{},"Node.js and npm installed",[36,51,52,53,46],{},"Firebase CLI installed (",[54,55,56],"code",{},"npm install -g firebase-tools",[36,58,59],{},"Basic understanding of React or your framework of choice",[61,62,63,66],"warning-box",{},[18,64,65],{},"Critical Security Note",[18,67,68],{},"Firebase Auth only verifies identity - it doesn't restrict data access. You MUST use Security Rules to control what authenticated users can access. Without proper rules, any authenticated user can read/write all your data.",[28,70,72],{"id":71},"step-by-step-guide","Step-by-Step Guide",[74,75,77,82,92,99,105],"step",{"number":76},"1",[78,79,81],"h3",{"id":80},"install-and-initialize-firebase","Install and initialize Firebase",[83,84,89],"pre",{"className":85,"code":87,"language":88},[86],"language-text","npm install firebase\n","text",[54,90,87],{"__ignoreMap":91},"",[18,93,94,95,98],{},"Create a Firebase config file (",[54,96,97],{},"lib/firebase.ts","):",[83,100,103],{"className":101,"code":102,"language":88},[86],"import { initializeApp } from 'firebase/app';\nimport { getAuth, connectAuthEmulator } from 'firebase/auth';\nimport { getFirestore, connectFirestoreEmulator } from 'firebase/firestore';\n\nconst firebaseConfig = {\n  apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY,\n  authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN,\n  projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID,\n  storageBucket: process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET,\n  messagingSenderId: process.env.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID,\n  appId: process.env.NEXT_PUBLIC_FIREBASE_APP_ID\n};\n\nconst app = initializeApp(firebaseConfig);\nexport const auth = getAuth(app);\nexport const db = getFirestore(app);\n\n// Use emulators in development\nif (process.env.NODE_ENV === 'development') {\n  connectAuthEmulator(auth, 'http://localhost:9099');\n  connectFirestoreEmulator(db, 'localhost', 8080);\n}\n",[54,104,102],{"__ignoreMap":91},[106,107,108],"tip-box",{},[18,109,110],{},"Tip:\nThe Firebase API key is safe to expose - it only identifies your project. Security comes from Security Rules, not hiding this key.",[74,112,114,118,121,145,151],{"number":113},"2",[78,115,117],{"id":116},"configure-authentication-providers","Configure authentication providers",[18,119,120],{},"In Firebase Console > Authentication > Sign-in method:",[122,123,124,132,138],"ol",{},[36,125,126,127,131],{},"Enable ",[128,129,130],"strong",{},"Email/Password"," (check \"Email link\" for passwordless option)",[36,133,126,134,137],{},[128,135,136],{},"Google"," (configure OAuth consent screen first)",[36,139,140,141,144],{},"Add ",[128,142,143],{},"Authorized domains"," for production",[83,146,149],{"className":147,"code":148,"language":88},[86],"# Required authorized domains:\nlocalhost (for development)\nyour-app.com\nyour-app.vercel.app\nyour-app.firebaseapp.com\n",[54,150,148],{"__ignoreMap":91},[61,152,153],{},[18,154,155],{},"Disable unused providers:\nOnly enable auth methods you actually use. Each enabled provider is a potential attack surface.",[74,157,159,163],{"number":158},"3",[78,160,162],{"id":161},"implement-secure-authentication-flows","Implement secure authentication flows",[83,164,167],{"className":165,"code":166,"language":88},[86],"import {\n  createUserWithEmailAndPassword,\n  signInWithEmailAndPassword,\n  signInWithPopup,\n  GoogleAuthProvider,\n  signOut,\n  sendEmailVerification,\n  sendPasswordResetEmail\n} from 'firebase/auth';\nimport { auth } from '@/lib/firebase';\n\n// Sign up with email/password\nasync function signUp(email: string, password: string) {\n  try {\n    const userCredential = await createUserWithEmailAndPassword(\n      auth,\n      email,\n      password\n    );\n\n    // Send verification email\n    await sendEmailVerification(userCredential.user);\n\n    return userCredential.user;\n  } catch (error: any) {\n    // Handle specific errors without revealing too much\n    if (error.code === 'auth/email-already-in-use') {\n      throw new Error('An account with this email already exists');\n    }\n    if (error.code === 'auth/weak-password') {\n      throw new Error('Password must be at least 6 characters');\n    }\n    throw new Error('Failed to create account');\n  }\n}\n\n// Sign in with email/password\nasync function signIn(email: string, password: string) {\n  try {\n    const userCredential = await signInWithEmailAndPassword(\n      auth,\n      email,\n      password\n    );\n    return userCredential.user;\n  } catch (error: any) {\n    // Don't reveal whether email exists\n    throw new Error('Invalid email or password');\n  }\n}\n\n// Sign in with Google\nasync function signInWithGoogle() {\n  const provider = new GoogleAuthProvider();\n  provider.setCustomParameters({\n    prompt: 'select_account'  // Always show account picker\n  });\n\n  try {\n    const result = await signInWithPopup(auth, provider);\n    return result.user;\n  } catch (error: any) {\n    if (error.code === 'auth/popup-closed-by-user') {\n      throw new Error('Sign-in cancelled');\n    }\n    throw new Error('Failed to sign in with Google');\n  }\n}\n\n// Sign out\nasync function logout() {\n  await signOut(auth);\n}\n\n// Password reset\nasync function resetPassword(email: string) {\n  try {\n    await sendPasswordResetEmail(auth, email);\n    // Always show success to prevent email enumeration\n  } catch (error) {\n    // Silently fail - don't reveal if email exists\n  }\n}\n",[54,168,166],{"__ignoreMap":91},[74,170,172,176],{"number":171},"4",[78,173,175],{"id":174},"handle-auth-state-properly","Handle auth state properly",[83,177,180],{"className":178,"code":179,"language":88},[86],"import { useEffect, useState } from 'react';\nimport { User, onAuthStateChanged, onIdTokenChanged } from 'firebase/auth';\nimport { auth } from '@/lib/firebase';\n\nexport function useAuth() {\n  const [user, setUser] = useState\u003CUser | null>(null);\n  const [loading, setLoading] = useState(true);\n\n  useEffect(() => {\n    // Listen for auth state changes\n    const unsubscribe = onAuthStateChanged(auth, (user) => {\n      setUser(user);\n      setLoading(false);\n    });\n\n    return () => unsubscribe();\n  }, []);\n\n  return { user, loading };\n}\n\n// For token-based auth state (catches token refresh)\nexport function useAuthWithToken() {\n  const [user, setUser] = useState\u003CUser | null>(null);\n  const [token, setToken] = useState\u003Cstring | null>(null);\n  const [loading, setLoading] = useState(true);\n\n  useEffect(() => {\n    const unsubscribe = onIdTokenChanged(auth, async (user) => {\n      if (user) {\n        const token = await user.getIdToken();\n        setToken(token);\n        setUser(user);\n      } else {\n        setToken(null);\n        setUser(null);\n      }\n      setLoading(false);\n    });\n\n    return () => unsubscribe();\n  }, []);\n\n  return { user, token, loading };\n}\n",[54,181,179],{"__ignoreMap":91},[74,183,185,189,196,202,205],{"number":184},"5",[78,186,188],{"id":187},"integrate-auth-with-firestore-security-rules","Integrate auth with Firestore Security Rules",[18,190,191,192,195],{},"Create ",[54,193,194],{},"firestore.rules",":",[83,197,200],{"className":198,"code":199,"language":88},[86],"rules_version = '2';\nservice cloud.firestore {\n  match /databases/{database}/documents {\n\n    // Helper function to check authentication\n    function isAuthenticated() {\n      return request.auth != null;\n    }\n\n    // Helper function to check if user owns document\n    function isOwner(userId) {\n      return isAuthenticated() && request.auth.uid == userId;\n    }\n\n    // Helper function to check email verification\n    function isEmailVerified() {\n      return isAuthenticated() && request.auth.token.email_verified == true;\n    }\n\n    // User profiles - users can only access their own\n    match /users/{userId} {\n      allow read: if isOwner(userId);\n      allow create: if isOwner(userId)\n                    && request.resource.data.keys().hasOnly(['email', 'displayName', 'createdAt'])\n                    && request.resource.data.email == request.auth.token.email;\n      allow update: if isOwner(userId)\n                    && !request.resource.data.diff(resource.data).affectedKeys()\n                        .hasAny(['email', 'createdAt', 'role']);\n      allow delete: if isOwner(userId);\n    }\n\n    // User's private documents\n    match /users/{userId}/documents/{docId} {\n      allow read, write: if isOwner(userId);\n    }\n\n    // Public posts - anyone can read, only verified authors can write\n    match /posts/{postId} {\n      allow read: if resource.data.status == 'published' || isOwner(resource.data.authorId);\n      allow create: if isEmailVerified()\n                    && request.resource.data.authorId == request.auth.uid;\n      allow update: if isOwner(resource.data.authorId);\n      allow delete: if isOwner(resource.data.authorId);\n    }\n  }\n}\n",[54,201,199],{"__ignoreMap":91},[18,203,204],{},"Deploy rules:",[83,206,209],{"className":207,"code":208,"language":88},[86],"firebase deploy --only firestore:rules\n",[54,210,208],{"__ignoreMap":91},[74,212,214,218,221],{"number":213},"6",[78,215,217],{"id":216},"verify-id-tokens-server-side","Verify ID tokens server-side",[18,219,220],{},"For API routes or serverless functions, always verify tokens:",[83,222,225],{"className":223,"code":224,"language":88},[86],"// Install Admin SDK\n// npm install firebase-admin\n\nimport { initializeApp, cert, getApps } from 'firebase-admin/app';\nimport { getAuth } from 'firebase-admin/auth';\n\n// Initialize Admin SDK (do once)\nif (!getApps().length) {\n  initializeApp({\n    credential: cert({\n      projectId: process.env.FIREBASE_PROJECT_ID,\n      clientEmail: process.env.FIREBASE_CLIENT_EMAIL,\n      privateKey: process.env.FIREBASE_PRIVATE_KEY?.replace(/\\\\n/g, '\\n')\n    })\n  });\n}\n\nconst adminAuth = getAuth();\n\n// Middleware to verify ID token\nasync function verifyAuth(req, res, next) {\n  const authHeader = req.headers.authorization;\n\n  if (!authHeader?.startsWith('Bearer ')) {\n    return res.status(401).json({ error: 'No token provided' });\n  }\n\n  const idToken = authHeader.split('Bearer ')[1];\n\n  try {\n    // Verify the ID token\n    const decodedToken = await adminAuth.verifyIdToken(idToken, true);\n\n    // Check if token was revoked\n    // The second param `true` already checks this, but be explicit\n    req.user = decodedToken;\n    next();\n  } catch (error: any) {\n    if (error.code === 'auth/id-token-expired') {\n      return res.status(401).json({ error: 'Token expired' });\n    }\n    if (error.code === 'auth/id-token-revoked') {\n      return res.status(401).json({ error: 'Token revoked' });\n    }\n    return res.status(401).json({ error: 'Invalid token' });\n  }\n}\n\n// Example API route\nexport default async function handler(req, res) {\n  await verifyAuth(req, res, () => {});\n  if (res.headersSent) return;\n\n  // User is authenticated\n  const userId = req.user.uid;\n\n  // Fetch user-specific data...\n  res.json({ userId, email: req.user.email });\n}\n",[54,226,224],{"__ignoreMap":91},[74,228,230,234,240],{"number":229},"7",[78,231,233],{"id":232},"implement-custom-claims-for-roles","Implement custom claims for roles",[83,235,238],{"className":236,"code":237,"language":88},[86],"// Server-side: Set custom claims (Admin SDK only)\nasync function setUserRole(uid: string, role: 'admin' | 'moderator' | 'user') {\n  await adminAuth.setCustomUserClaims(uid, { role });\n\n  // Optionally revoke existing tokens to force refresh\n  await adminAuth.revokeRefreshTokens(uid);\n}\n\n// Example: Promote user to admin\nawait setUserRole('user-uid-here', 'admin');\n\n// Client-side: Force token refresh to get new claims\nasync function refreshToken() {\n  const user = auth.currentUser;\n  if (user) {\n    await user.getIdToken(true);  // Force refresh\n  }\n}\n\n// Client-side: Check claims\nasync function getUserRole(): Promise\u003Cstring | null> {\n  const user = auth.currentUser;\n  if (!user) return null;\n\n  const tokenResult = await user.getIdTokenResult();\n  return tokenResult.claims.role as string || 'user';\n}\n\n// Security Rules: Use custom claims\n// firestore.rules\nmatch /admin/{document=**} {\n  allow read, write: if request.auth != null\n                     && request.auth.token.role == 'admin';\n}\n\nmatch /moderation/{document=**} {\n  allow read, write: if request.auth != null\n                     && request.auth.token.role in ['admin', 'moderator'];\n}\n",[54,239,237],{"__ignoreMap":91},[61,241,242],{},[18,243,244],{},"Never trust client-set data for roles.\nCustom claims can only be set by the Admin SDK on the server. If you store roles in Firestore, always verify them in Security Rules, never in client code.",[74,246,248,252],{"number":247},"8",[78,249,251],{"id":250},"create-user-profile-on-signup","Create user profile on signup",[83,253,256],{"className":254,"code":255,"language":88},[86],"import { doc, setDoc, serverTimestamp } from 'firebase/firestore';\nimport { db } from '@/lib/firebase';\n\n// Option 1: Create profile after signup (client-side)\nasync function createUserProfile(user: User) {\n  const userRef = doc(db, 'users', user.uid);\n\n  await setDoc(userRef, {\n    email: user.email,\n    displayName: user.displayName || '',\n    photoURL: user.photoURL || '',\n    createdAt: serverTimestamp()\n  });\n}\n\n// Call after successful signup\nconst user = await signUp(email, password);\nawait createUserProfile(user);\n\n// Option 2: Use Cloud Function (recommended for consistency)\n// functions/src/index.ts\nimport * as functions from 'firebase-functions';\nimport * as admin from 'firebase-admin';\n\nadmin.initializeApp();\n\nexport const onUserCreated = functions.auth.user().onCreate(async (user) => {\n  const userRef = admin.firestore().doc(`users/${user.uid}`);\n\n  await userRef.set({\n    email: user.email,\n    displayName: user.displayName || '',\n    photoURL: user.photoURL || '',\n    createdAt: admin.firestore.FieldValue.serverTimestamp(),\n    role: 'user'  // Default role\n  });\n});\n",[54,257,255],{"__ignoreMap":91},[259,260,262],"h4",{"id":261},"security-checklist","Security Checklist",[33,264,265,272,278,281,284,287,290,293],{},[36,266,267,268,271],{},"Security Rules use ",[54,269,270],{},"request.auth"," to verify ownership",[36,273,274,275,46],{},"No rules allow unrestricted read/write (",[54,276,277],{},"allow read, write: if true",[36,279,280],{},"ID tokens are verified server-side for sensitive operations",[36,282,283],{},"Custom claims are set only via Admin SDK, never client-side",[36,285,286],{},"Email verification is required for important actions",[36,288,289],{},"Error messages don't reveal whether emails exist",[36,291,292],{},"Only necessary auth providers are enabled",[36,294,295],{},"Authorized domains are properly configured",[28,297,299],{"id":298},"how-to-verify-it-worked","How to Verify It Worked",[122,301,302,308,314,320,326,332],{},[36,303,304,307],{},[128,305,306],{},"Test sign-up:"," Create account, verify email is sent, profile is created",[36,309,310,313],{},[128,311,312],{},"Test rules:"," Use Firebase Console Rules Playground to simulate requests",[36,315,316,319],{},[128,317,318],{},"Test unauthorized access:"," Try to read another user's data - should fail",[36,321,322,325],{},[128,323,324],{},"Test token verification:"," Call API with expired/invalid token - should return 401",[36,327,328,331],{},[128,329,330],{},"Test custom claims:"," Set admin claim, verify it appears in token and rules work",[36,333,334,337,338],{},[128,335,336],{},"Run emulator tests:"," ",[54,339,340],{},"firebase emulators:exec \"npm test\"",[28,342,344],{"id":343},"common-errors-troubleshooting","Common Errors & Troubleshooting",[259,346,348],{"id":347},"error-missing-or-insufficient-permissions","Error: \"Missing or insufficient permissions\"",[18,350,351],{},"Your Security Rules are blocking the operation. Check: 1) User is authenticated, 2) Rules allow the specific operation, 3) Document path matches the rule pattern.",[259,353,355],{"id":354},"error-authpopup-blocked","Error: \"auth/popup-blocked\"",[18,357,358],{},"Browser blocked the OAuth popup. Ensure signInWithPopup is called directly from a user interaction (click handler), not from async code.",[259,360,362],{"id":361},"error-authnetwork-request-failed","Error: \"auth/network-request-failed\"",[18,364,365],{},"Network issue or Firebase services blocked. Check firewall settings, or try signInWithRedirect instead of signInWithPopup.",[259,367,369],{"id":368},"custom-claims-not-appearing","Custom claims not appearing",[18,371,372,373,376],{},"The client needs to force a token refresh with ",[54,374,375],{},"getIdToken(true)"," after claims are set. Claims only update when the token is refreshed.",[259,378,380],{"id":379},"admin-sdk-permission-denied","Admin SDK \"Permission denied\"",[18,382,383,384,387],{},"Check service account credentials. Ensure the private key is properly formatted (replace ",[54,385,386],{},"\\\\n"," with actual newlines) and the service account has required IAM roles.",[389,390,391,398,412,418],"faq-section",{},[392,393,395],"faq-item",{"question":394},"Should I use Firestore or Realtime Database with Auth?",[18,396,397],{},"Firestore is recommended for most apps - it has better querying, offline support, and more expressive Security Rules. Use Realtime Database only if you need real-time sync for simple data structures or cheaper pricing for high-volume reads.",[392,399,401],{"question":400},"How do I handle session persistence?",[18,402,403,404,407,408,411],{},"Firebase Auth handles this automatically. By default, sessions persist in indexedDB (web) or secure storage (mobile). You can change this with ",[54,405,406],{},"setPersistence()"," - use ",[54,409,410],{},"browserSessionPersistence"," for stricter security on shared devices.",[392,413,415],{"question":414},"When should I verify tokens server-side vs trust client auth state?",[18,416,417],{},"Always verify server-side for: database writes, payment processing, admin actions, or any sensitive operation. Client auth state is fine for: showing/hiding UI elements, displaying user info, non-sensitive reads protected by Security Rules.",[392,419,421],{"question":420},"How do I migrate from anonymous auth to permanent accounts?",[18,422,423,424,427],{},"Use ",[54,425,426],{},"linkWithCredential()"," to link an anonymous account to email/password or OAuth. This preserves the user's UID and all associated data. Always prompt users to convert before they leave.",[18,429,430,433,437,438,437,442,437,446],{},[128,431,432],{},"Related guides:",[40,434,436],{"href":435},"/blog/how-to/firebase-security-rules","Firebase Security Rules"," ·\n",[40,439,441],{"href":440},"/blog/how-to/firebase-auth-rules","Firebase Auth Rules",[40,443,445],{"href":444},"/blog/how-to/protect-routes","Protect Routes & API Endpoints",[40,447,449],{"href":448},"/blog/guides/firebase","Firebase Security Guide",[451,452,453,459,464],"related-articles",{},[454,455],"related-card",{"description":456,"href":457,"title":458},"Step-by-step guide to preventing SQL injection. Parameterized queries, ORMs, input validation, and common mistakes that ","/blog/how-to/prevent-sql-injection","How to Prevent SQL Injection in Your App",[454,460],{"description":461,"href":462,"title":463},"Step-by-step guide to securing your Prisma ORM setup. Prevent injection attacks, handle raw queries safely, and implemen","/blog/how-to/prisma-security","How to Secure Prisma ORM",[454,465],{"description":466,"href":467,"title":468},"Step-by-step guide to preventing XSS in React and Next.js. Sanitizing user input, Content Security Policy, and common XS","/blog/how-to/protect-against-xss","How to Protect Against XSS Attacks",{"title":91,"searchDepth":470,"depth":470,"links":471},2,[472,473,484,485],{"id":30,"depth":470,"text":31},{"id":71,"depth":470,"text":72,"children":474},[475,477,478,479,480,481,482,483],{"id":80,"depth":476,"text":81},3,{"id":116,"depth":476,"text":117},{"id":161,"depth":476,"text":162},{"id":174,"depth":476,"text":175},{"id":187,"depth":476,"text":188},{"id":216,"depth":476,"text":217},{"id":232,"depth":476,"text":233},{"id":250,"depth":476,"text":251},{"id":298,"depth":470,"text":299},{"id":343,"depth":470,"text":344},"how-to","2026-01-12","Step-by-step guide to setting up Firebase Authentication securely. Configure providers, integrate security rules, verify tokens server-side, and implement custom claims.",false,"md",null,"yellow",{},true,"Complete guide to secure Firebase Authentication with security rules integration.","/blog/how-to/firebase-auth","[object Object]","HowTo",{"title":5,"description":488},{"loc":496},"blog/how-to/firebase-auth",[],"summary_large_image","iadC5FKqtxew-u_SejlnqjjiSab_V01B-C7EoqjUImM",1775843928721]