# How to Use Firebase Auth with Next.js

- Tool: Firebase
- Difficulty: Intermediate
- Time required: 20-25 min
- Compatibility: Next.js 14+ (App Router), Firebase JS SDK v9+, Firebase Admin SDK 12+
- Last updated: March 2026

## TL;DR

To use Firebase Auth with Next.js, initialize the client-side Firebase SDK for sign-in flows and the Firebase Admin SDK for server-side session verification in API routes and middleware. Use onAuthStateChanged in a React context provider to manage client-side auth state, and verify ID tokens with admin.auth().verifyIdToken() on the server. Pass the ID token as a cookie or Authorization header for server-side authentication in App Router components.

## Implementing Firebase Authentication in Next.js

Next.js applications run code on both the client and server, which means Firebase Auth requires two SDKs: the client SDK for sign-in UI and the Admin SDK for server-side token verification. This tutorial shows you how to configure both SDKs, create an AuthContext provider with onAuthStateChanged, verify ID tokens in API routes, use middleware for route protection, and implement a session cookie pattern for seamless server-side authentication in the App Router.

## Before you start

- Next.js 14+ project with App Router
- Firebase project with Authentication enabled
- Firebase JS SDK v9+ and firebase-admin installed
- Email/Password sign-in enabled in Firebase Console

## Step-by-step guide

### 1. Set up Firebase client SDK configuration

Create a Firebase client configuration file that initializes the app and auth instances. This file will be imported by client components for sign-in, sign-out, and auth state listening. Store your Firebase config values in environment variables prefixed with NEXT_PUBLIC_ so they are available on the client side.

```
// lib/firebase.ts
import { initializeApp, getApps } from 'firebase/app';
import { getAuth } from 'firebase/auth';

const firebaseConfig = {
  apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY,
  authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN,
  projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID,
  storageBucket: process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET,
  messagingSenderId: process.env.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID,
  appId: process.env.NEXT_PUBLIC_FIREBASE_APP_ID,
};

// Prevent re-initialization in hot reload
const app = getApps().length === 0 ? initializeApp(firebaseConfig) : getApps()[0];
export const auth = getAuth(app);
export default app;
```

**Expected result:** Firebase client SDK is initialized once and exported for use in client components.

### 2. Set up Firebase Admin SDK for server-side verification

The Admin SDK runs on the server to verify ID tokens and manage users. It requires a service account key stored as a server-side environment variable (not NEXT_PUBLIC_). Initialize it once and export the admin auth instance for use in API routes and server components.

```
// lib/firebase-admin.ts
import { initializeApp, getApps, cert } from 'firebase-admin/app';
import { getAuth } from 'firebase-admin/auth';

const adminApp =
  getApps().length === 0
    ? initializeApp({
        credential: cert({
          projectId: process.env.FIREBASE_PROJECT_ID,
          clientEmail: process.env.FIREBASE_CLIENT_EMAIL,
          privateKey: process.env.FIREBASE_PRIVATE_KEY?.replace(/\\n/g, '\n'),
        }),
      })
    : getApps()[0];

export const adminAuth = getAuth(adminApp);
```

**Expected result:** Firebase Admin SDK is initialized and can verify ID tokens on the server.

### 3. Create an AuthContext provider for client-side state

Build a React context that wraps onAuthStateChanged and provides the current user to all client components. Mark this as a client component with 'use client'. The provider also exposes a loading state so components can show a loading indicator while the initial auth check completes.

```
'use client';

import { createContext, useContext, useEffect, useState } from 'react';
import { onAuthStateChanged, User } from 'firebase/auth';
import { auth } from '@/lib/firebase';

interface AuthContextType {
  user: User | null;
  loading: boolean;
}

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

export function AuthProvider({ children }: { children: React.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 const useAuth = () => useContext(AuthContext);
```

**Expected result:** Client components can call useAuth() to access the current user and loading state reactively.

### 4. Implement sign-in and sign-out functions

Create authentication functions that handle email/password sign-in and sign-out. After a successful sign-in, get the user's ID token and store it in an HTTP-only cookie via an API route so the server can access it. This bridges client-side Firebase Auth with server-side authentication.

```
'use client';

import {
  signInWithEmailAndPassword,
  createUserWithEmailAndPassword,
  signOut as firebaseSignOut,
} from 'firebase/auth';
import { auth } from '@/lib/firebase';

export async function signIn(email: string, password: string) {
  const credential = await signInWithEmailAndPassword(auth, email, password);
  const idToken = await credential.user.getIdToken();

  // Store token in HTTP-only cookie via API route
  await fetch('/api/auth/session', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ idToken }),
  });

  return credential.user;
}

export async function signUp(email: string, password: string) {
  const credential = await createUserWithEmailAndPassword(auth, email, password);
  const idToken = await credential.user.getIdToken();

  await fetch('/api/auth/session', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ idToken }),
  });

  return credential.user;
}

export async function signOut() {
  await firebaseSignOut(auth);
  await fetch('/api/auth/session', { method: 'DELETE' });
}
```

**Expected result:** Users can sign in and sign out, with session cookies set for server-side authentication.

### 5. Create the session API route

Build an API route that receives the Firebase ID token from the client, verifies it with the Admin SDK, creates a session cookie, and sets it as an HTTP-only cookie. This cookie is automatically sent with every request, enabling server-side authentication in middleware and server components.

```
// app/api/auth/session/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { adminAuth } from '@/lib/firebase-admin';
import { cookies } from 'next/headers';

export async function POST(request: NextRequest) {
  const { idToken } = await request.json();

  // Verify the ID token
  try {
    await adminAuth.verifyIdToken(idToken);
  } catch {
    return NextResponse.json({ error: 'Invalid token' }, { status: 401 });
  }

  // Create a session cookie (14-day expiry)
  const expiresIn = 60 * 60 * 24 * 14 * 1000; // 14 days
  const sessionCookie = await adminAuth.createSessionCookie(idToken, { expiresIn });

  const cookieStore = await cookies();
  cookieStore.set('session', sessionCookie, {
    maxAge: expiresIn / 1000,
    httpOnly: true,
    secure: process.env.NODE_ENV === 'production',
    path: '/',
    sameSite: 'lax',
  });

  return NextResponse.json({ status: 'success' });
}

export async function DELETE() {
  const cookieStore = await cookies();
  cookieStore.delete('session');
  return NextResponse.json({ status: 'success' });
}
```

**Expected result:** The API route creates a session cookie on sign-in and deletes it on sign-out.

### 6. Protect server components with session verification

In server components and middleware, read the session cookie and verify it with the Admin SDK. If the session is invalid or missing, redirect to the login page. This pattern works in any server component, API route, or Next.js middleware.

```
// lib/auth-server.ts
import { cookies } from 'next/headers';
import { adminAuth } from '@/lib/firebase-admin';
import { DecodedIdToken } from 'firebase-admin/auth';

export async function getServerUser(): Promise<DecodedIdToken | null> {
  const cookieStore = await cookies();
  const session = cookieStore.get('session')?.value;
  if (!session) return null;

  try {
    return await adminAuth.verifySessionCookie(session, true);
  } catch {
    return null;
  }
}

// Usage in a server component:
// app/dashboard/page.tsx
import { redirect } from 'next/navigation';
import { getServerUser } from '@/lib/auth-server';

export default async function DashboardPage() {
  const user = await getServerUser();
  if (!user) redirect('/login');

  return <div>Welcome, {user.email}</div>;
}
```

**Expected result:** Server components can verify the user's identity and redirect unauthenticated requests to the login page.

## Complete code example

File: `lib/firebase.ts`

```typescript
// lib/firebase.ts — Client SDK
import { initializeApp, getApps } from 'firebase/app';
import { getAuth } from 'firebase/auth';

const firebaseConfig = {
  apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY,
  authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN,
  projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID,
  storageBucket: process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET,
  messagingSenderId: process.env.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID,
  appId: process.env.NEXT_PUBLIC_FIREBASE_APP_ID,
};

const app = getApps().length === 0
  ? initializeApp(firebaseConfig)
  : getApps()[0];

export const auth = getAuth(app);
export default app;

// lib/firebase-admin.ts — Admin SDK
import { initializeApp as initAdmin, getApps as getAdminApps, cert } from 'firebase-admin/app';
import { getAuth as getAdminAuth } from 'firebase-admin/auth';

const adminApp = getAdminApps().length === 0
  ? initAdmin({
      credential: cert({
        projectId: process.env.FIREBASE_PROJECT_ID,
        clientEmail: process.env.FIREBASE_CLIENT_EMAIL,
        privateKey: process.env.FIREBASE_PRIVATE_KEY?.replace(/\\n/g, '\n'),
      }),
    })
  : getAdminApps()[0];

export const adminAuth = getAdminAuth(adminApp);

// lib/auth-server.ts — Server-side session helper
import { cookies } from 'next/headers';

export async function getServerUser() {
  const cookieStore = await cookies();
  const session = cookieStore.get('session')?.value;
  if (!session) return null;
  try {
    return await adminAuth.verifySessionCookie(session, true);
  } catch {
    return null;
  }
}
```

## Common mistakes

- **Importing firebase-admin in client components, causing build errors from Node.js-only modules** — undefined Fix: Never import firebase-admin in files marked 'use client' or imported by client components. Keep admin imports strictly in API routes, server components, and middleware.
- **Using NEXT_PUBLIC_ prefix for the Firebase service account private key, exposing it to the client** — undefined Fix: Server-only environment variables must NOT have the NEXT_PUBLIC_ prefix. Use FIREBASE_PRIVATE_KEY without the prefix so it is only available on the server.
- **Relying only on client-side auth checks without server-side verification, allowing bypasses** — undefined Fix: Always verify the session cookie or ID token with the Admin SDK on the server. Client-side auth state can be spoofed by modifying JavaScript.
- **Not handling the private key newline conversion when storing it in environment variables** — undefined Fix: Apply .replace(/\\n/g, '\n') to convert literal \n strings back to actual newline characters in the private key.

## Best practices

- Separate client SDK (lib/firebase.ts) and Admin SDK (lib/firebase-admin.ts) into different files to prevent accidental client-side imports
- Use session cookies instead of raw ID tokens for server-side auth — session cookies last up to 14 days vs 1 hour for ID tokens
- Store Firebase config in environment variables: NEXT_PUBLIC_ for client config, no prefix for server secrets
- Use the getApps().length check to prevent re-initialization during hot module replacement
- Verify session cookies with checkRevoked: true to ensure revoked sessions are rejected
- Wrap the root layout with AuthProvider to give all client components access to auth state

## Frequently asked questions

### Do I need both the client SDK and Admin SDK for Next.js?

Yes. The client SDK handles sign-in UI and auth state on the browser. The Admin SDK verifies tokens and manages sessions on the server. Both are needed for a secure Next.js authentication flow.

### Why use session cookies instead of just sending ID tokens?

Firebase ID tokens expire after 1 hour. Session cookies created with createSessionCookie() can last up to 14 days and support revocation. They are also sent automatically with every request, unlike Authorization headers.

### Can I use Firebase Auth with Next.js Server Actions?

Yes. Server Actions run on the server, so you can read the session cookie and verify it with the Admin SDK just like in API routes.

### How do I handle token refresh in Next.js?

The client SDK handles token refresh automatically. For server-side, session cookies do not need refresh — they are valid for their full expiry period. Re-create the session cookie when the user explicitly signs in again.

### Is Firebase Auth compatible with Next.js middleware?

Yes, but with limitations. Middleware runs on the Edge runtime, which does not support the full Admin SDK. Use a lightweight JWT verification library or call an API route from middleware for full token verification.

### Can RapidDev help implement authentication in my Next.js application?

Yes. RapidDev can implement complete Firebase Auth flows in Next.js including social login, session management, role-based access control, and middleware-based route protection.

---

Source: https://www.rapidevelopers.com/firebase-tutorial/how-to-use-firebase-auth-with-next-js
© RapidDev — https://www.rapidevelopers.com/firebase-tutorial/how-to-use-firebase-auth-with-next-js
