Firebase Auth with Next.js App Router — The Right Way
Firebase Authentication and the Next.js App Router have a rocky relationship. Client SDK state doesn't naturally flow into Server Components, and most tutorials skip middleware altogether. Here's the pattern we've landed on.
The Core Problem
Firebase Auth is client-side by default. onAuthStateChanged fires in the browser, not on the server. To protect routes in Server Components and middleware, you need session cookies.
Session Architecture
Browser → signIn() → getIdToken() → POST /api/auth/session
← Set-Cookie: __session (httpOnly, secure)
Middleware → verifySessionCookie() → allow / redirect
Server Components → read cookie → call Admin SDK → get user
Setting Up Firebase Admin
// lib/firebase-admin.ts
import { initializeApp, getApps, cert } from 'firebase-admin/app';
import { getAuth } from 'firebase-admin/auth';
const app =
getApps().length > 0
? getApps()[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'),
}),
});
export const adminAuth = getAuth(app);
Session Cookie Route
// app/api/auth/session/route.ts
import { NextResponse } from 'next/server';
import { adminAuth } from '@/lib/firebase-admin';
export async function POST(req: Request) {
const { idToken } = await req.json();
const expiresIn = 60 * 60 * 24 * 5 * 1000; // 5 days
const sessionCookie = await adminAuth.createSessionCookie(idToken, {
expiresIn,
});
const response = NextResponse.json({ status: 'success' });
response.cookies.set('__session', sessionCookie, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
maxAge: expiresIn / 1000,
path: '/',
});
return response;
}
Middleware Route Protection
// middleware.ts
import { NextRequest, NextResponse } from 'next/server';
const PROTECTED_ROUTES = ['/dashboard', '/settings', '/api/protected'];
export async function middleware(req: NextRequest) {
const session = req.cookies.get('__session')?.value;
const isProtected = PROTECTED_ROUTES.some((r) =>
req.nextUrl.pathname.startsWith(r)
);
if (isProtected && !session) {
return NextResponse.redirect(new URL('/login', req.url));
}
return NextResponse.next();
}
export const config = {
matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'],
};
Warning
Never verify the session cookie in middleware itself — Admin SDK calls are too slow for edge runtime. Use a lightweight cookie presence check in middleware and full verification in Server Components or API routes.