Firebase Authentication identifies users in the browser, but production authorization must happen on a trusted server. In Next.js App Router, that means exchanging a Firebase ID token for a secure session cookie, then verifying that cookie with the Firebase Admin SDK before returning protected data.
sequenceDiagram
participant B as Browser
participant F as Firebase Auth
participant N as Next.js server
B->>F: Sign in
F-->>B: ID token
B->>N: POST session with ID token
N->>N: Verify token and create session cookie
N-->>B: Set HttpOnly cookie
B->>N: Request protected resource
N->>N: Verify session cookie
Initialize Firebase Admin safely
Install firebase-admin and keep its credentials server-only. Never prefix service-account variables with NEXT_PUBLIC_ or import this module into a Client Component.
// src/lib/firebase-admin.ts
import "server-only";
import { cert, getApps, initializeApp } from "firebase-admin/app";
function required(name: string): string {
const value = process.env[name];
if (!value) throw new Error(`Missing environment variable: ${name}`);
return value;
}
export const adminApp =
getApps()[0] ??
initializeApp({
credential: cert({
projectId: required("FIREBASE_PROJECT_ID"),
clientEmail: required("FIREBASE_CLIENT_EMAIL"),
privateKey: required("FIREBASE_PRIVATE_KEY").replace(/\\n/g, "\n"),
}),
});
The getApps() guard prevents duplicate initialization during development reloads. On Google-managed infrastructure, Application Default Credentials are preferable to storing a service-account key; explicit credentials remain useful for local development and other hosting providers.
Exchange an ID token for a session cookie
After signing in with the Firebase client SDK, call user.getIdToken() and send the result to a route handler. The server verifies it before issuing an HTTP-only cookie.
// app/api/session/route.ts
import { getAuth } from "firebase-admin/auth";
import { NextResponse } from "next/server";
import { adminApp } from "@/lib/firebase-admin";
export const runtime = "nodejs";
const expiresIn = 5 * 24 * 60 * 60 * 1000;
export async function POST(request: Request) {
if (request.headers.get("origin") !== process.env.APP_ORIGIN) {
return NextResponse.json({ error: "Invalid origin" }, { status: 403 });
}
const authorization = request.headers.get("authorization");
const idToken = authorization?.startsWith("Bearer ")
? authorization.slice(7)
: null;
if (!idToken) {
return NextResponse.json({ error: "Missing token" }, { status: 401 });
}
try {
const auth = getAuth(adminApp);
const decoded = await auth.verifyIdToken(idToken, true);
// Prevent an old stolen token from being upgraded to a session.
if (Date.now() / 1000 - decoded.auth_time > 5 * 60) {
return NextResponse.json({ error: "Recent sign-in required" }, { status: 401 });
}
const session = await auth.createSessionCookie(idToken, { expiresIn });
const response = NextResponse.json({ ok: true });
response.cookies.set("__session", session, {
httpOnly: true,
secure: process.env.NODE_ENV === "production",
sameSite: "lax",
path: "/",
maxAge: expiresIn / 1000,
});
return response;
} catch {
return NextResponse.json({ error: "Invalid token" }, { status: 401 });
}
}
Origin validation reduces login CSRF risk, while HttpOnly prevents browser JavaScript from reading the session. Session cookies do not renew automatically, so choose an expiry that balances usability and risk, then require users to authenticate again when necessary.
Verify every protected request
Centralize verification so Server Components, Server Actions, and route handlers share the same trust boundary.
// src/lib/current-user.ts
import "server-only";
import { cookies } from "next/headers";
import { getAuth } from "firebase-admin/auth";
import { adminApp } from "./firebase-admin";
export async function getCurrentUser() {
const value = (await cookies()).get("__session")?.value;
if (!value) return null;
try {
return await getAuth(adminApp).verifySessionCookie(value, true);
} catch {
return null;
}
}
Passing true checks revocation, which improves logout and account-disable enforcement but can add a network lookup. For authorization, inspect trusted custom claims from the decoded token; never accept roles or user IDs supplied separately by the client.
Firebase Admin requires the Node.js runtime, so do not perform authoritative verification in Edge Middleware. Middleware may redirect based on cookie presence for convenience, but the destination must still verify the cookie. Also avoid caching personalized responses across users.
Conclusion
A secure App Router design keeps Firebase client authentication in the browser and places trust in Node.js server code. Verify tokens, issue hardened cookies, re-check sessions on protected operations, and make authorization decisions only from server-verified claims.