Firebase Auth with Nextjs App Router

Firebase Auth with Next.js App Router

We may earn an affiliate commission through purchases made from our guides and tutorials.

Server Components (RSC) cannot rely on the client’s in-memory Firebase state. The reliable pattern is to sign in on the client with the Firebase Web SDK, exchange the ID token for a session cookie on the server, and read that cookie in RSC. Firebase officially supports minting and verifying session cookies with the Admin SDK, with configurable lifetimes from 5 min to 14 days.

Implications. Treat the session cookie as your authoritative server credential in RSC and Route Handlers. Keep any Admin-SDK work in the Node.js runtime, not Edge, because the Admin SDK depends on Node modules unavailable in Edge.

Minimal flow

  1. Client signs in with the Firebase Web SDK and obtains an ID token.
  2. Client posts the ID token to a server endpoint you control.
  3. Server calls createSessionCookie(idToken, { expiresIn }), sets an HttpOnly, Secure cookie, and returns.
  4. RSC and Route Handlers read and verify the cookie with verifySessionCookie.

Notes for 2025. Next.js 15 is stable, Server Actions and next/headers cookies() support setting cookies in Actions and Route Handlers. Prefer Node.js runtime for any Admin SDK usage. Server Actions are public endpoints; validate input and origin just like an API route.

Client sign-in (extract)

Authenticate on the client, then exchange the ID token. Keep the ID token out of URLs.

// app/(auth)/login/actions.ts
"use client";
import { signInWithEmailAndPassword } from "firebase/auth";
import { auth } from "@/externals/firebase/client";

export async function login(email: string, password: string) {
  const { user } = await signInWithEmailAndPassword(auth, email, password);
  const idToken = await user.getIdToken(/* forceRefresh? false */);

  // Exchange via POST; do not put tokens in query strings.
  const res = await fetch("/api/session", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ idToken }),
    credentials: "include",
  });
  if (!res.ok) throw new Error("Session exchange failed");
}

Session exchange (Route Handler, Node.js runtime)

Route Handlers make CSRF and origin checks straightforward and keep Admin SDK off the Edge runtime.

// app/api/session/route.ts
export const runtime = "nodejs";
import { NextResponse } from "next/server";
import { z } from "zod";
import { adminAuth } from "@/externals/firebase/server";

const Body = z.object({ idToken: z.string().min(20) });

export async function POST(req: Request) {
  // Basic CSRF / origin gate
  const origin = req.headers.get("origin") ?? "";
  if (!origin || !new URL(origin).host.endsWith(process.env.NEXT_PUBLIC_ALLOWED_HOST!)) {
    return NextResponse.json({ error: "Bad origin" }, { status: 400 });
  }

  const json = await req.json().catch(() => ({}));
  const parse = Body.safeParse(json);
  if (!parse.success) return NextResponse.json({ error: "Bad payload" }, { status: 400 });

  // 1 h is fine, but session cookies can be longer; cap at <= 14 days per Firebase.
  const expiresIn = 60 * 60 * 1000; // 1 hour
  const sessionCookie = await adminAuth.createSessionCookie(parse.data.idToken, { expiresIn });

  const res = NextResponse.json({ ok: true });
  res.cookies.set({
    name: "session",
    value: sessionCookie,
    httpOnly: true,
    secure: true,
    sameSite: "lax", // consider "strict" if UX allows
    maxAge: expiresIn / 1000,
    path: "/",
  });
  return res;
}

Why Node.js runtime. Firebase Admin requires Node built-ins; Edge runtime lacks them. This avoids os/fs module errors.

Reading the user in Server Components

Verify once per request in a tiny helper. Use checkRevoked: true if you need immediate revocation semantics.

// lib/server/current-user.ts
export const runtime = "nodejs";
import { cookies } from "next/headers";
import { adminAuth } from "@/externals/firebase/server";

export async function getCurrentUser() {
  const cookieStore = await cookies();
  const session = cookieStore.get("session")?.value;
  if (!session) return null;

  try {
    const decoded = await adminAuth.verifySessionCookie(session, true); // checkRevoked = true
    return { uid: decoded.uid, email: decoded.email ?? null };
  } catch {
    return null;
  }
}

Implications. checkRevoked: true adds a revocation lookup. Use it for admin areas and payment flows, or set it to false for lower-risk reads to reduce latency.

Logout that actually logs out

Clear the cookie server-side. Optionally revoke refresh tokens to force all sessions closed, and sign out the client to clear in-memory state.

// app/api/session/route.ts (DELETE)
export const runtime = "nodejs";
import { NextResponse } from "next/server";
import { adminAuth } from "@/externals/firebase/server";
import { cookies } from "next/headers";

export async function DELETE() {
  const cookieStore = await cookies();
  const session = cookieStore.get("session")?.value;

  if (session) {
    try {
      const { uid } = await adminAuth.verifySessionCookie(session);
      await adminAuth.revokeRefreshTokens(uid); // optional but strong
    } catch { /* ignore */ }
  }

  const res = NextResponse.json({ ok: true });
  res.cookies.set({
    name: "session",
    value: "",
    httpOnly: true,
    secure: true,
    sameSite: "lax",
    expires: new Date(0),
    path: "/",
  });
  return res;
}

Client hook:

// app/(auth)/logout.ts
"use client";
import { signOut } from "firebase/auth";
import { auth } from "@/externals/firebase/client";

export async function logout() {
  await fetch("/api/session", { method: "DELETE", credentials: "include" });
  await signOut(auth); // clear client state
}

Firebase documents revoking refresh tokens and verifying session cookies during sign-out.

RSC integration pattern

In any Server Component or loader, call your helper and branch on the result:

// app/dashboard/page.tsx
import { getCurrentUser } from "@/lib/server/current-user";

export default async function Dashboard() {
  const user = await getCurrentUser();
  if (!user) {
    // Render a login CTA or redirect via a client boundary.
    return <p>Sign in to continue.</p>;
  }
  return <div>Hello {user.email ?? user.uid}</div>;
}

Guardrails and optimizations

Choose sensible expirations. Session cookies can last up to 14 days, independent of the 1 h ID token lifetime. Pick shorter for admin areas; consider longer for consumer apps with strict revocation checks at critical endpoints.

Keep Admin SDK off Edge. Do not use Admin SDK in Middleware or Edge Route Handlers; use Node runtime endpoints. If you need auth hints in Middleware for routing, rely on lightweight signals and defer verification to Node handlers.

Harden the session exchange. Treat Server Actions and Route Handlers as public APIs. Validate payloads, require JSON POST, check Origin or Host, and rate-limit if exposed to the internet.

Cookie settings. Always set HttpOnly, Secure, SameSite=Lax or Strict, and Path=/. Consider Domain only if you intentionally share across subdomains. Use Strict where you can to reduce CSRF exposure. Official Next.js cookies() supports reads in RSC and writes in Actions/Route Handlers.

Understand scope limits. Firebase session cookies do not authenticate client SDK calls to Firestore or Realtime Database. Use your server as the gateway for privileged operations when you choose this model.

Stay current with Next.js. Next.js 15.x emphasizes stability in App Router and Server Actions; the patterns above match current guidance.

When to pick Firebase Auth over other providers

Supabase Auth, Clerk, and Auth.js integrate well with Next.js, yet Firebase Auth remains an effective choice when you already use Firebase, need Google’s identity providers, or prefer managed MFA and action links. Session cookies give you a clean RSC-friendly, server-centric contract without hydrating client state in every component.

Was this helpful?

Thanks for your feedback!
Alex is the resident editor and oversees all of the guides published. His past work and experience include Colorlib, Stack Diary, Hostvix, and working with a number of editorial publications. He has been wrangling code and publishing his findings about it since the early 2000s.

Leave a comment

Your email address will not be published. Required fields are marked *