Skip to main content
RapidDev - Software Development Agency
firebase-tutorial

How to Restrict Access to Logged-in Users in Firebase

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.

What you'll learn

  • How to write Firestore security rules that require authentication
  • How to write Storage security rules that require authentication
  • How to build a client-side auth guard with onAuthStateChanged()
  • How to combine server-side rules with client-side route protection
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Beginner8 min read10-15 minFirebase JS SDK v9+, Firestore and Storage (all plans)March 2026RapidDev Engineering Team
TL;DR

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

1

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.

typescript
1rules_version = '2';
2service cloud.firestore {
3 match /databases/{database}/documents {
4
5 // Global: require auth for all collections
6 match /{document=**} {
7 allow read, write: if request.auth != null;
8 }
9
10 // Better: per-collection rules with ownership
11 match /profiles/{userId} {
12 allow read: if request.auth != null;
13 allow write: if request.auth != null
14 && request.auth.uid == userId;
15 }
16
17 match /posts/{postId} {
18 allow read: if request.auth != null;
19 allow create: if request.auth != null
20 && request.resource.data.authorId == request.auth.uid;
21 allow update, delete: if request.auth != null
22 && 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.

2

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.

typescript
1rules_version = '2';
2service firebase.storage {
3 match /b/{bucket}/o {
4 // User files: only the owner can access
5 match /users/{userId}/{allPaths=**} {
6 allow read, write: if request.auth != null
7 && request.auth.uid == userId;
8 }
9
10 // Shared files: any authenticated user can read
11 match /shared/{allPaths=**} {
12 allow read: if request.auth != null;
13 allow write: if request.auth != null
14 && 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.

3

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.

typescript
1import { getAuth, onAuthStateChanged, User } from 'firebase/auth'
2
3const auth = getAuth()
4
5onAuthStateChanged(auth, (user: User | null) => {
6 if (user) {
7 console.log('Signed in as:', user.email)
8 // Show authenticated UI
9 } else {
10 console.log('Not signed in.')
11 // Redirect to login page
12 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.

4

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.

typescript
1import { useState, useEffect, ReactNode } from 'react'
2import { getAuth, onAuthStateChanged, User } from 'firebase/auth'
3
4interface AuthGuardProps {
5 children: ReactNode
6 fallback?: ReactNode
7}
8
9export function AuthGuard({ children, fallback }: AuthGuardProps) {
10 const [user, setUser] = useState<User | null>(null)
11 const [loading, setLoading] = useState(true)
12
13 useEffect(() => {
14 const unsubscribe = onAuthStateChanged(getAuth(), (user) => {
15 setUser(user)
16 setLoading(false)
17 })
18 return () => unsubscribe()
19 }, [])
20
21 if (loading) {
22 return <div>Loading...</div>
23 }
24
25 if (!user) {
26 return fallback ?? <div>Please <a href="/login">sign in</a> to continue.</div>
27 }
28
29 return <>{children}</>
30}
31
32// 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.

5

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.

typescript
1import { getAuth, onAuthStateChanged, User } from 'firebase/auth'
2
3function waitForAuth(): Promise<User | null> {
4 return new Promise((resolve) => {
5 const unsubscribe = onAuthStateChanged(getAuth(), (user) => {
6 unsubscribe()
7 resolve(user)
8 })
9 })
10}
11
12// Use before any Firestore query
13async function loadDashboardData() {
14 const user = await waitForAuth()
15
16 if (!user) {
17 console.log('Not authenticated. Redirect to login.')
18 return
19 }
20
21 // Now safe to query — auth token is attached
22 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

auth-guard.tsx
1import { useState, useEffect, createContext, useContext, ReactNode } from 'react'
2import { initializeApp } from 'firebase/app'
3import { getAuth, onAuthStateChanged, User } from 'firebase/auth'
4
5const 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})
10
11const auth = getAuth(app)
12
13interface AuthContextValue {
14 user: User | null
15 loading: boolean
16}
17
18const AuthContext = createContext<AuthContextValue>({
19 user: null,
20 loading: true
21})
22
23export function AuthProvider({ children }: { children: ReactNode }) {
24 const [user, setUser] = useState<User | null>(null)
25 const [loading, setLoading] = useState(true)
26
27 useEffect(() => {
28 const unsubscribe = onAuthStateChanged(auth, (user) => {
29 setUser(user)
30 setLoading(false)
31 })
32 return () => unsubscribe()
33 }, [])
34
35 return (
36 <AuthContext.Provider value={{ user, loading }}>
37 {children}
38 </AuthContext.Provider>
39 )
40}
41
42export function useAuth() {
43 return useContext(AuthContext)
44}
45
46export function AuthGuard({
47 children,
48 fallback
49}: {
50 children: ReactNode
51 fallback?: ReactNode
52}) {
53 const { user, loading } = useAuth()
54
55 if (loading) return <div>Loading...</div>
56 if (!user) return fallback ?? <div>Please sign in.</div>
57 return <>{children}</>
58}
59
60export 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.

ChatGPT Prompt

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.

Firebase Prompt

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.

RapidDev

Talk to an Expert

Our team has built 600+ apps. Get personalized help with your project.

Book a free consultation

Need help with your project?

Our experts have built 600+ apps and can accelerate your development. Book a free consultation — no strings attached.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.