Restrict access to logged-in users in Firebase by combining security rules with client-side route protection. In Firestore security rules, use request.auth != null to deny unauthenticated reads and writes. In your app, use onAuthStateChanged() to detect the auth state and redirect unauthenticated users away from protected pages. Both layers are required — security rules protect your data, while client-side guards protect the user experience.
Restricting Access to Logged-in Users in Firebase
Many apps need to restrict data access and page visibility to authenticated users only. Firebase provides two complementary mechanisms: server-side security rules that reject requests from unauthenticated clients, and client-side auth state detection that controls what pages users can see. This tutorial covers both, showing how to lock down Firestore, Storage, and your app's UI to signed-in users.
Prerequisites
- A Firebase project with Authentication and Firestore enabled
- At least one sign-in method configured in Firebase Console
- Firebase JS SDK v9+ installed (npm install firebase)
- A basic React, Next.js, or similar frontend project
Step-by-step guide
Write Firestore rules that require authentication
Write Firestore rules that require authentication
The simplest authenticated-only rule checks request.auth != null. This ensures that only signed-in users can read or write data. You can apply this globally or per collection. For production apps, combine auth checks with additional conditions like user ownership (request.auth.uid == resource.data.userId) to ensure users only access their own data.
1rules_version = '2';2service cloud.firestore {3 match /databases/{database}/documents {45 // Global: require auth for all collections6 match /{document=**} {7 allow read, write: if request.auth != null;8 }910 // Better: per-collection rules with ownership11 match /profiles/{userId} {12 allow read: if request.auth != null;13 allow write: if request.auth != null14 && request.auth.uid == userId;15 }1617 match /posts/{postId} {18 allow read: if request.auth != null;19 allow create: if request.auth != null20 && request.resource.data.authorId == request.auth.uid;21 allow update, delete: if request.auth != null22 && resource.data.authorId == request.auth.uid;23 }24 }25}Expected result: Unauthenticated requests to Firestore are rejected with 'Missing or insufficient permissions'. Authenticated users can read all data and write only their own.
Write Storage rules that require authentication
Write Storage rules that require authentication
Apply the same pattern to Firebase Storage rules. Check request.auth != null to restrict file access to signed-in users. Scope file paths to user UIDs so each user can only access their own files. Storage rules use the same request.auth object as Firestore rules.
1rules_version = '2';2service firebase.storage {3 match /b/{bucket}/o {4 // User files: only the owner can access5 match /users/{userId}/{allPaths=**} {6 allow read, write: if request.auth != null7 && request.auth.uid == userId;8 }910 // Shared files: any authenticated user can read11 match /shared/{allPaths=**} {12 allow read: if request.auth != null;13 allow write: if request.auth != null14 && request.resource.size < 10 * 1024 * 1024;15 }16 }17}Expected result: Unauthenticated Storage requests are denied. Each user can only access their own files, and shared files are readable by all signed-in users.
Detect auth state on the client with onAuthStateChanged()
Detect auth state on the client with onAuthStateChanged()
Use onAuthStateChanged() to listen for authentication state changes. The callback receives the user object when signed in or null when signed out. This listener fires once on page load with the current state and then again whenever the user signs in or out. Use this to control what the user sees in your app.
1import { getAuth, onAuthStateChanged, User } from 'firebase/auth'23const auth = getAuth()45onAuthStateChanged(auth, (user: User | null) => {6 if (user) {7 console.log('Signed in as:', user.email)8 // Show authenticated UI9 } else {10 console.log('Not signed in.')11 // Redirect to login page12 window.location.href = '/login'13 }14})Expected result: The callback fires with the current user on page load and whenever auth state changes, enabling you to show or hide content accordingly.
Build a React auth guard component
Build a React auth guard component
Create a reusable component that wraps protected routes. It listens to auth state, shows a loading spinner while auth initializes, redirects to login if not authenticated, and renders the protected content when the user is signed in. This pattern works with React Router, Next.js, or any React-based framework.
1import { useState, useEffect, ReactNode } from 'react'2import { getAuth, onAuthStateChanged, User } from 'firebase/auth'34interface AuthGuardProps {5 children: ReactNode6 fallback?: ReactNode7}89export function AuthGuard({ children, fallback }: AuthGuardProps) {10 const [user, setUser] = useState<User | null>(null)11 const [loading, setLoading] = useState(true)1213 useEffect(() => {14 const unsubscribe = onAuthStateChanged(getAuth(), (user) => {15 setUser(user)16 setLoading(false)17 })18 return () => unsubscribe()19 }, [])2021 if (loading) {22 return <div>Loading...</div>23 }2425 if (!user) {26 return fallback ?? <div>Please <a href="/login">sign in</a> to continue.</div>27 }2829 return <>{children}</>30}3132// Usage:33// <AuthGuard>34// <Dashboard />35// </AuthGuard>Expected result: Protected content only renders when the user is signed in. Loading state is shown during auth initialization. Unauthenticated users see a login prompt.
Wait for auth before making Firestore queries
Wait for auth before making Firestore queries
The most common cause of 'Missing or insufficient permissions' errors is querying Firestore before Firebase Auth has initialized. Auth state is asynchronous — it may take a moment after page load before the user's session is restored. Always wait for onAuthStateChanged to fire before executing queries. Create a helper that returns a promise resolving when auth is ready.
1import { getAuth, onAuthStateChanged, User } from 'firebase/auth'23function waitForAuth(): Promise<User | null> {4 return new Promise((resolve) => {5 const unsubscribe = onAuthStateChanged(getAuth(), (user) => {6 unsubscribe()7 resolve(user)8 })9 })10}1112// Use before any Firestore query13async function loadDashboardData() {14 const user = await waitForAuth()1516 if (!user) {17 console.log('Not authenticated. Redirect to login.')18 return19 }2021 // Now safe to query — auth token is attached22 const snapshot = await getDocs(23 query(collection(db, 'posts'), where('authorId', '==', user.uid))24 )25 return snapshot.docs.map((doc) => ({ id: doc.id, ...doc.data() }))26}Expected result: Firestore queries only execute after auth is ready, preventing 'Missing or insufficient permissions' errors from race conditions.
Complete working example
1import { useState, useEffect, createContext, useContext, ReactNode } from 'react'2import { initializeApp } from 'firebase/app'3import { getAuth, onAuthStateChanged, User } from 'firebase/auth'45const app = initializeApp({6 apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY!,7 authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN!,8 projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID!9})1011const auth = getAuth(app)1213interface AuthContextValue {14 user: User | null15 loading: boolean16}1718const AuthContext = createContext<AuthContextValue>({19 user: null,20 loading: true21})2223export function AuthProvider({ children }: { children: ReactNode }) {24 const [user, setUser] = useState<User | null>(null)25 const [loading, setLoading] = useState(true)2627 useEffect(() => {28 const unsubscribe = onAuthStateChanged(auth, (user) => {29 setUser(user)30 setLoading(false)31 })32 return () => unsubscribe()33 }, [])3435 return (36 <AuthContext.Provider value={{ user, loading }}>37 {children}38 </AuthContext.Provider>39 )40}4142export function useAuth() {43 return useContext(AuthContext)44}4546export function AuthGuard({47 children,48 fallback49}: {50 children: ReactNode51 fallback?: ReactNode52}) {53 const { user, loading } = useAuth()5455 if (loading) return <div>Loading...</div>56 if (!user) return fallback ?? <div>Please sign in.</div>57 return <>{children}</>58}5960export function waitForAuth(): Promise<User | null> {61 return new Promise((resolve) => {62 const unsub = onAuthStateChanged(auth, (user) => {63 unsub()64 resolve(user)65 })66 })67}Common mistakes when restricting Access to Logged-in Users in Firebase
Why it's a problem: Making Firestore queries before onAuthStateChanged fires, causing 'Missing or insufficient permissions' errors
How to avoid: Always wait for the first onAuthStateChanged callback before querying Firestore. Use a loading state or a waitForAuth() promise to ensure auth is initialized.
Why it's a problem: Relying only on client-side route guards without server-side security rules
How to avoid: Client-side guards only protect the UI — a determined attacker can bypass them. Always enforce access control in Firestore and Storage security rules. Both layers are required.
Why it's a problem: Using auth.currentUser directly on page load when it may still be null during auth initialization
How to avoid: Use onAuthStateChanged() instead of checking auth.currentUser directly. The currentUser property is null until auth initialization completes, which is asynchronous.
Best practices
- Always enforce authentication in both security rules (server-side) and client UI (client-side) for defense in depth
- Use onAuthStateChanged() to detect auth state rather than checking auth.currentUser directly
- Show a loading state while auth initializes to prevent flash of unauthenticated content
- Create a reusable AuthProvider context and AuthGuard component to avoid duplicating auth logic across pages
- Wait for auth initialization before making any Firestore or Storage requests to avoid permission errors
- Use request.auth.uid in security rules to scope data access to the authenticated user's own documents
- For email-verified-only access, add request.auth.token.email_verified == true to your security rules
Still stuck?
Copy one of these prompts to get a personalized, step-by-step explanation.
I have a Firebase app with Authentication and Firestore. Show me how to restrict all data access to logged-in users only, including Firestore security rules with request.auth != null, a React AuthGuard component using onAuthStateChanged, and a pattern for waiting until auth is ready before making queries.
Build a complete React auth guard system for a Firebase app that includes an AuthProvider context, useAuth hook, AuthGuard wrapper component, and a waitForAuth utility. Use Firebase modular SDK v9+ with TypeScript. Include Firestore security rules that require authentication and user ownership.
Frequently asked questions
Why do I get 'Missing or insufficient permissions' even though the user is logged in?
The most common cause is querying Firestore before auth initialization completes. onAuthStateChanged is asynchronous — if you query before it fires, the request has no auth token and gets rejected. Wait for the first callback before making queries.
Are client-side auth guards enough to protect my data?
No. Client-side guards only control the UI. Anyone can bypass them using browser developer tools or direct API calls. You must also set server-side security rules in Firestore and Storage to actually protect your data. Think of client-side guards as UX, and server-side rules as security.
Can I require email verification in addition to being logged in?
Yes. In your security rules, add request.auth.token.email_verified == true alongside request.auth != null. On the client, check user.emailVerified after onAuthStateChanged fires and redirect unverified users to a verification page.
How do I restrict access to admin users only?
Set custom claims on admin users using the Firebase Admin SDK server-side. Then check request.auth.token.admin == true in your security rules. On the client, access custom claims via user.getIdTokenResult() after sign-in.
Does onAuthStateChanged work across browser tabs?
Yes, when using the default browserLocalPersistence. If a user signs out in one tab, onAuthStateChanged fires in all other tabs with null. This keeps the UI consistent across tabs.
Can RapidDev help implement authentication and access control in my Firebase app?
Yes. RapidDev can set up Firebase Auth with security rules, client-side auth guards, role-based access control, and protected routes tailored to your application's requirements.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation