# How to Restrict Access to Logged-in Users in Firebase

- Tool: Firebase
- Difficulty: Beginner
- Time required: 10-15 min
- Compatibility: Firebase JS SDK v9+, Firestore and Storage (all plans)
- Last updated: March 2026

## 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.

## Before you start

- 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.

```
rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {

    // Global: require auth for all collections
    match /{document=**} {
      allow read, write: if request.auth != null;
    }

    // Better: per-collection rules with ownership
    match /profiles/{userId} {
      allow read: if request.auth != null;
      allow write: if request.auth != null
                   && request.auth.uid == userId;
    }

    match /posts/{postId} {
      allow read: if request.auth != null;
      allow create: if request.auth != null
                    && request.resource.data.authorId == request.auth.uid;
      allow update, delete: if request.auth != null
                            && resource.data.authorId == request.auth.uid;
    }
  }
}
```

**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.

```
rules_version = '2';
service firebase.storage {
  match /b/{bucket}/o {
    // User files: only the owner can access
    match /users/{userId}/{allPaths=**} {
      allow read, write: if request.auth != null
                         && request.auth.uid == userId;
    }

    // Shared files: any authenticated user can read
    match /shared/{allPaths=**} {
      allow read: if request.auth != null;
      allow write: if request.auth != null
                   && request.resource.size < 10 * 1024 * 1024;
    }
  }
}
```

**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.

```
import { getAuth, onAuthStateChanged, User } from 'firebase/auth'

const auth = getAuth()

onAuthStateChanged(auth, (user: User | null) => {
  if (user) {
    console.log('Signed in as:', user.email)
    // Show authenticated UI
  } else {
    console.log('Not signed in.')
    // Redirect to login page
    window.location.href = '/login'
  }
})
```

**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.

```
import { useState, useEffect, ReactNode } from 'react'
import { getAuth, onAuthStateChanged, User } from 'firebase/auth'

interface AuthGuardProps {
  children: ReactNode
  fallback?: ReactNode
}

export function AuthGuard({ children, fallback }: AuthGuardProps) {
  const [user, setUser] = useState<User | null>(null)
  const [loading, setLoading] = useState(true)

  useEffect(() => {
    const unsubscribe = onAuthStateChanged(getAuth(), (user) => {
      setUser(user)
      setLoading(false)
    })
    return () => unsubscribe()
  }, [])

  if (loading) {
    return <div>Loading...</div>
  }

  if (!user) {
    return fallback ?? <div>Please <a href="/login">sign in</a> to continue.</div>
  }

  return <>{children}</>
}

// Usage:
// <AuthGuard>
//   <Dashboard />
// </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.

```
import { getAuth, onAuthStateChanged, User } from 'firebase/auth'

function waitForAuth(): Promise<User | null> {
  return new Promise((resolve) => {
    const unsubscribe = onAuthStateChanged(getAuth(), (user) => {
      unsubscribe()
      resolve(user)
    })
  })
}

// Use before any Firestore query
async function loadDashboardData() {
  const user = await waitForAuth()

  if (!user) {
    console.log('Not authenticated. Redirect to login.')
    return
  }

  // Now safe to query — auth token is attached
  const snapshot = await getDocs(
    query(collection(db, 'posts'), where('authorId', '==', user.uid))
  )
  return snapshot.docs.map((doc) => ({ id: doc.id, ...doc.data() }))
}
```

**Expected result:** Firestore queries only execute after auth is ready, preventing 'Missing or insufficient permissions' errors from race conditions.

## Complete code example

File: `auth-guard.tsx`

```typescript
import { useState, useEffect, createContext, useContext, ReactNode } from 'react'
import { initializeApp } from 'firebase/app'
import { getAuth, onAuthStateChanged, User } from 'firebase/auth'

const app = initializeApp({
  apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY!,
  authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN!,
  projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID!
})

const auth = getAuth(app)

interface AuthContextValue {
  user: User | null
  loading: boolean
}

const AuthContext = createContext<AuthContextValue>({
  user: null,
  loading: true
})

export function AuthProvider({ children }: { children: ReactNode }) {
  const [user, setUser] = useState<User | null>(null)
  const [loading, setLoading] = useState(true)

  useEffect(() => {
    const unsubscribe = onAuthStateChanged(auth, (user) => {
      setUser(user)
      setLoading(false)
    })
    return () => unsubscribe()
  }, [])

  return (
    <AuthContext.Provider value={{ user, loading }}>
      {children}
    </AuthContext.Provider>
  )
}

export function useAuth() {
  return useContext(AuthContext)
}

export function AuthGuard({
  children,
  fallback
}: {
  children: ReactNode
  fallback?: ReactNode
}) {
  const { user, loading } = useAuth()

  if (loading) return <div>Loading...</div>
  if (!user) return fallback ?? <div>Please sign in.</div>
  return <>{children}</>
}

export function waitForAuth(): Promise<User | null> {
  return new Promise((resolve) => {
    const unsub = onAuthStateChanged(auth, (user) => {
      unsub()
      resolve(user)
    })
  })
}
```

## Common mistakes

- **Making Firestore queries before onAuthStateChanged fires, causing 'Missing or insufficient permissions' errors** — undefined Fix: Always wait for the first onAuthStateChanged callback before querying Firestore. Use a loading state or a waitForAuth() promise to ensure auth is initialized.
- **Relying only on client-side route guards without server-side security rules** — undefined Fix: 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.
- **Using auth.currentUser directly on page load when it may still be null during auth initialization** — undefined Fix: 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

## 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.

---

Source: https://www.rapidevelopers.com/firebase-tutorial/how-to-restrict-access-to-logged-in-users-in-firebase
© RapidDev — https://www.rapidevelopers.com/firebase-tutorial/how-to-restrict-access-to-logged-in-users-in-firebase
