Use Firebase Admin SDK in Nextjs Route Handlers for secure token verification

Use Firebase Admin SDK in Next.js Route Handlers for secure token verification

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

Most teams reach for Firebase Authentication to offload sign-in and identity. The client SDK issues short-lived ID tokens, but your server still needs to verify them before doing anything sensitive. In a Next.js app with Route Handlers, the safest pattern is: obtain an ID token on the client, send it on each request, and verify it on the server with the Firebase Admin SDK. This guide walks you through an end-to-end setup with current Next.js and Firebase. You will deploy to a standard Node.js environment. If you plan to host on DigitalOcean App Platform, create a DigitalOcean account first so you can add environment variables during setup.

What you will build:

You will initialize a singleton Firebase Admin instance, create a typed Route Handler that verifies Authorization: Bearer <ID_TOKEN>, and return data only for authenticated users. You will also learn how to avoid the Edge runtime for these routes, handle common verification errors, and optionally check for revoked tokens to enforce logout across devices.

Requirements and assumptions

Use Next.js 15 or newer, which ships stable App Router Route Handlers. Your secure routes must run on the Node.js runtime because Firebase Admin depends on Node core modules that are not available on the Edge runtime. This is true across providers that implement Edge isolation. If you need middleware level checks at the edge, use lightweight header presence checks only and delegate full verification to a Node handler.

1) Install packages

Install the Admin SDK in your project.

npm install firebase-admin
npm install --save-dev server-only

server-only is a tiny guard that prevents accidental client bundles from importing server code.

2) Configure credentials safely

Prefer Workload Identity on cloud platforms. If you use a service account key, store it as environment variables. Many platforms require escaping newlines in PRIVATE_KEY, so normalize the value at runtime as shown below. Never commit keys to your repository. To deploy on DigitalOcean App Platform, add these as “Environment Variables” in your component settings before your first deploy.

  • FIREBASE_PROJECT_ID
  • FIREBASE_CLIENT_EMAIL
  • FIREBASE_PRIVATE_KEY (keep the literal \n newlines or paste exact multiline text)

3) Create a singleton Admin SDK initializer

Next.js may instantiate modules multiple times during development or across serverless invocations. Create a module that exports a single Admin app and Auth instance. This prevents “already exists” errors and avoids wasted connections.

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

function getPrivateKey() {
  const key = process.env.FIREBASE_PRIVATE_KEY;
  if (!key) throw new Error('FIREBASE_PRIVATE_KEY is not set');
  // Support both plain multiline and \n-escaped formats
  return key.replace(/\\n/g, '\n');
}

export function adminApp() {
  if (getApps().length) return getApp();
  return initializeApp({
    credential: cert({
      projectId: process.env.FIREBASE_PROJECT_ID,
      clientEmail: process.env.FIREBASE_CLIENT_EMAIL,
      privateKey: getPrivateKey(),
    }),
  });
}

export function adminAuth() {
  return getAuth(adminApp());
}

The getApps() check ensures a single initialized app. This is the recommended pattern for serverless and dev hot reload.

4) Add a typed token extractor

Clients will send an ID token in the Authorization header. Build a small utility that parses it and returns clear errors.

// app/lib/auth-headers.ts
export type Bearer = { scheme: 'Bearer'; token: string };

export function parseAuthorization(header: string | null): Bearer {
  if (!header) throw new Error('Missing Authorization header');
  const [scheme, token] = header.split(' ');
  if (scheme !== 'Bearer' || !token) throw new Error('Malformed Authorization header');
  return { scheme: 'Bearer', token };
}

5) Implement a secure Route Handler

Create a Route Handler in the App Router and force the Node runtime. Verify the ID token with the Admin SDK, handle expected failures, and return JSON.

// app/api/secure/route.ts
import { NextResponse } from 'next/server';
import { adminAuth } from '@/app/lib/firebase-admin';
import { parseAuthorization } from '@/app/lib/auth-headers';

export const runtime = 'nodejs'; // Admin SDK requires Node APIs

type AuthedResponse = {
  uid: string;
  email?: string;
  customClaims?: Record<string, unknown>;
};

export async function GET(request: Request) {
  try {
    const { token } = parseAuthorization(request.headers.get('authorization'));
    // Verify signature, audience, issuer, expiry
    const decoded = await adminAuth().verifyIdToken(token);
    // Optionally load custom claims or user profile here
    const result: AuthedResponse = {
      uid: decoded.uid,
      email: decoded.email,
      customClaims: decoded,
    };
    return NextResponse.json(result, { status: 200 });
  } catch (err: unknown) {
    // Map common Firebase Auth errors to HTTP
    const message = err instanceof Error ? err.message : 'Unauthorized';
    const code = message.includes('expired') ? 401 : 401;
    return NextResponse.json({ error: 'Unauthorized', detail: message }, { status: code });
  }
}

verifyIdToken validates the token issuer, audience, signature with Google public keys, and expiry. If verification fails, respond with 401 and a stable error body.

6) Enforce global sign-out with token revocation checks

If you want sign-out to invalidate previously issued tokens immediately, pass checkRevoked: true. This adds a revocation lookup and is suitable for admin panels or high-risk actions. On failure, prompt the client to refresh credentials or reauthenticate.

// app/api/secure/route.ts (variant)
const decoded = await adminAuth().verifyIdToken(token, true);
// If you reach this point, the token is valid and not revoked

Revocation checks require an extra call and can add latency. Use them selectively on routes that change data or access sensitive information.

7) Client: send the ID token on each request

On the client, after authentication with the Firebase web SDK, request the current ID token and send it in the Authorization header. Refresh on 401.

// app/(client)/useAuthedFetch.ts
import { getAuth } from 'firebase/auth';

export async function authedFetch(input: RequestInfo, init: RequestInit = {}) {
  const auth = getAuth();
  const user = auth.currentUser;
  const idToken = user ? await user.getIdToken(/* forceRefresh? */ false) : null;
  const headers = new Headers(init.headers);
  if (idToken) headers.set('Authorization', `Bearer ${idToken}`);
  const res = await fetch(input, { ...init, headers });
  if (res.status === 401 && user) {
    // Token may be expired or revoked; attempt a refresh once
    const fresh = await user.getIdToken(true);
    headers.set('Authorization', `Bearer ${fresh}`);
    return fetch(input, { ...init, headers });
  }
  return res;
}

Next.js will bundle this only for the client tree. Your server code that imports firebase-admin is isolated by server-only.

8) Common pitfalls and how to avoid them

Edge runtime errors. If you see errors about fs, os, or other Node modules, the route is running on the Edge runtime. Export runtime = 'nodejs' from the handler. Do not import firebase-admin from middleware. As of recent Next.js releases, Node middleware exists, but prefer verification inside Node Route Handlers where you control dependencies and error handling. (Next.js)

Multiple initializations. Reuse the app with getApps() and a lazy initializer, as shown earlier. This prevents “The default Firebase app already exists” errors and keeps memory pressure low across cold starts.

Clock skew and expiry. ID tokens are short-lived. Expect 401 for expired tokens and request a fresh token on the client as shown. verifyIdToken handles signature rotation automatically by fetching Google public keys.

Custom claims and authorization. Use custom claims on the user to drive authorization decisions and include a quick claim snapshot in your response. For changes to take effect, ask the client to refresh the ID token.

9) Optional: route-level guard helper

For many APIs, you will reuse the same verification block. Extract a guard that returns the decoded token or a NextResponse error.

// app/lib/with-auth.ts
import { NextResponse } from 'next/server';
import { adminAuth } from '@/app/lib/firebase-admin';
import { parseAuthorization } from '@/app/lib/auth-headers';
import type { DecodedIdToken } from 'firebase-admin/auth';

export async function requireUser(
  request: Request,
  opts: { checkRevoked?: boolean } = {}
): Promise<{ token: DecodedIdToken } | { error: NextResponse }> {
  try {
    const { token } = parseAuthorization(request.headers.get('authorization'));
    const decoded = await adminAuth().verifyIdToken(token, !!opts.checkRevoked);
    return { token: decoded };
  } catch (e) {
    return { error: NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) };
  }
}

Use it inside handlers to keep code short and consistent.

// app/api/orders/route.ts
import { NextResponse } from 'next/server';
import { requireUser } from '@/app/lib/with-auth';

export const runtime = 'nodejs';

export async function POST(request: Request) {
  const result = await requireUser(request, { checkRevoked: true });
  if ('error' in result) return result.error;

  const { token } = result;
  // Example authorization check
  if (token.admin !== true) {
    return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
  }

  // Continue with work that needs a verified user
  return NextResponse.json({ ok: true });
}

10) Local and deployment notes

Local development. Use a .env.local file and restart the dev server after adding secrets. Never expose Admin credentials with NEXT_PUBLIC_ prefixes.

Deployment. For DigitalOcean App Platform, set environment variables on the app component and select a Node.js runtime. For platforms that support file mounts, you may supply GOOGLE_APPLICATION_CREDENTIALS pointing to a mounted JSON file. For most teams, environment variables are simpler.

HTTP testing. You can smoke-test with curl by pasting a token from your browser after signing in. Replace <TOKEN> below.

curl -i http://localhost:3000/api/secure \
  -H "Authorization: Bearer <TOKEN>"

A 200 response returns your Firebase uid. A 401 response indicates a missing, malformed, expired, or revoked token.

11) Production checklist

  • Make every handler that imports firebase-admin export runtime = 'nodejs'.
  • Keep a single Admin app per process using the initializer shown earlier.
  • Decide where to use checkRevoked: true to enforce sign-out for sensitive routes.
  • Do not use Admin SDK in Edge middleware. Route to a Node handler for verification.
  • Keep keys out of git and normalize newline format for PRIVATE_KEY.

Where this leaves you

You now have a stable pattern for secure token verification in Next.js Route Handlers using the Firebase Admin SDK. The approach scales across serverless and traditional Node deployments, plays well with short-lived ID tokens, and supports immediate revocation when needed. Start with a single authenticated route, deploy it to your Node runtime on DigitalOcean or your preferred host, and extend the guard to the rest of your API as you add authorization rules.

Appendix: Reference types

If you want stronger typing on the decoded token and responses, you can import DecodedIdToken from the Admin SDK and define your own claim map.

// app/types/auth.ts
import type { DecodedIdToken } from 'firebase-admin/auth';

export type Claims = DecodedIdToken & {
  role?: 'user' | 'editor' | 'admin';
};

export type ApiError = { error: string; detail?: string };

This helps keep server code self-documenting as your authorization model grows.

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 *