Setting Up Firebase Authentication with Next.js 13 App Router Using Server Components
UPDATED ON 16th November 2023
Firebase is an efficient platform for adding authentication to web applications, while Next.js 13.5, with its server components, allows developers to write React components rendered on the server.
What is this guide about
- Logging in a user on the front end by utilizing the Firebase API.
- Upon successful login, transfer the access token to the Next.js backend.
- Generating a cookie in the backend and sending it back to the front end.
- Storing the cookie on the front end.
- Implementing middleware on the frontend to include the cookie with every request sent to the backend.
- The backend checks the validity of the cookie for each request and redirects to the login page if the cookie is invalid.
I’ve spent hours figuring it out so I hope it will help you save some time.

Requirements:
- A new Next.js project 13.5+ has been created using
npx create-react-app my-app
. - TypeScript and the latest App Router are being used.
- A Firebase project has been set up.
Step-by-step Implementation:
1. Install Required Packages:
For the frontend:
npm i firebase
For the backend:
npm i firebase-admin
2. Set up Firebase Configuration:
Create two files for configuration:
- /lib/firebase-admin-config.ts (Backend)
- /lib/firebase-config.ts (Frontend)
Firebase Admin Config (Backend):
In /lib/firebase-admin-config.ts
, paste the following:
import * as admin from "firebase-admin";
import { initializeApp, getApps, cert } from 'firebase-admin/app';
import serviceAccountJson from '@/service-account.json.json'
import { getAuth } from "firebase-admin/auth";
const serviceAccount = serviceAccountJson as admin.ServiceAccount;
const firebaseAdminConfig = {
credential: cert(serviceAccount)
}
export function customInitApp() {
if (getApps().length <= 0) {
return initializeApp({
credential: applicationDefault(),
});
} else {
return getApps()[0];
}
}
export const adminAuth = customInitApp();
Notice the customInitApp() this will ensure that no errors such as ‘app/duplicate-app’ will arise when calling firebase-admin.
Make sure to replace serviceAccountJson
with the path to your service account key file. The service key can be created on the Google Cloud Console here. The service key is created along with Firebase project.
Place it in src/service-account.json

Firebase Config (Frontend):
In /lib/firebase-config.ts
, paste:
import { initializeApp } from "firebase/app";
import { getApps, getApp } from "firebase/app";
import { getAuth } from "firebase/auth";
import { GoogleAuthProvider } from "firebase/auth";
import { CollectionReference, addDoc, collection, doc, getFirestore, setDoc } from "firebase/firestore";
import { query, getDocs, where } from "firebase/firestore";
const firebaseConfig = {
apiKey: "XXXXXX",
authDomain: "XXXXXX",
projectId: "XXXXXX",
storageBucket: "XXXXXX",
messagingSenderId: "XXXXXX",
appId: "1:XXXXXX:web:XXXXXX",
measurementId: "G-XXXXXX",
};
export const app = getApps().length > 0 ? getApp() : initializeApp(firebaseConfig);
export const auth = getAuth(app);
export const db = getFirestore(app);
export const provider = new GoogleAuthProvider();
Replace the placeholders with your Firebase app’s credentials.
You can find this at https://console.firebase.google.com/u/0/ under the ‘Project Overview’ section. Navigate to the ‘General’ tab and look within your web app settings.

3. Implement Authentication API:
In the src/app/api/auth/route.ts
file, you'll have the API routes handling authentication. The GET
method checks if a request is authenticated, and the POST
method handles user sign-in
import { auth } from "firebase-admin";
// import { customInitApp } from "@/lib/firebase-admin-config";
import { cookies, headers } from "next/headers";
import { NextRequest, NextResponse } from "next/server";
customInitApp() // Important otherwise you will receive no-app error
export async function GET(request: NextRequest) {
const session = cookies().get("session")?.value || "";
//Validate if the cookie exist in the request
if (!session) {
return NextResponse.json({ isLogged: false }, { status: 401 });
}
//Use Firebase Admin to validate the session cookie
const decodedClaims = await auth().verifySessionCookie(session, true);
if (!decodedClaims) {
return NextResponse.json({ isLogged: false }, { status: 401 });
}
return NextResponse.json({ isLogged: true }, { status: 200 });
}
export async function POST(request: NextRequest, response: NextResponse) {
const authorization = headers().get("Authorization");
if (authorization?.startsWith("Bearer ")) {
const idToken = authorization.split("Bearer ")[1];
const decodedToken = await auth().verifyIdToken(idToken);
if (decodedToken) {
//Generate session cookie
const expiresIn = 60 * 60 * 24 * 5 * 1000;
const sessionCookie = await auth().createSessionCookie(idToken, {
expiresIn,
});
const options = {
name: "session",
value: sessionCookie,
maxAge: expiresIn,
httpOnly: true,
secure: true,
};
//Add the cookie to the browser
cookies().set(options);
}
}
return NextResponse.json({}, { status: 200 });
}
In Next.js, naming a file as route.ts
within the /api
directory makes it accessible via the endpoint http://localhost:3000/api/auth
. This naming convention is important for Next.js to recognize and treat the file as a valid API endpoint.
The GET method is designed to verify if a request contains a valid session token. If it doesn’t, it responds with isLogged: false
.
This verification will play a crucial role in the middleware.ts
we'll set up later, as it will use the GET method to validate each server request.
On the other hand, the POST method manages user sign-ins. If the sign-in credentials are valid, a session gets created on the server and is then sent to the frontend browser to be stored in its headers. Subsequently, with every browser request, this cookie will be sent in the headers. The middleware, using the GET method, will then validate these requests. In essence, this is the authentication process simplified.
4. Set Up Middleware:
The middleware in src/middleware.ts
ensures that only authenticated users can access certain routes. It checks for a valid session on every request and redirects unauthenticated users to the login page.
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
export async function middleware(request: NextRequest, response: NextResponse) {
const session = request.cookies.get("session");
//Return to /login if don't have a session
if (!session) {
return NextResponse.redirect(new URL("/login", request.url));
}
//Call the authentication endpoint
const responseAPI = await fetch(`${request.nextUrl.origin}/api/auth`, {
headers: {
Cookie: `session=${session?.value}`,
},
});
//Return to /login if token is not authorized
if (responseAPI.status !== 200) {
return NextResponse.redirect(new URL("/login", request.url));
}
return NextResponse.next();
}
//Add your protected routes
export const config = {
matcher: ["/app/:path*"],
};
middleware.ts runs on client side. In case you will run Node code you will receive error:
Error: The edge runtime does not support Node.js 'os' module
.
For every request to the server, if no session exists, the user is redirected to the login page, as seen in the first line. If a session is detected, the request, along with the session cookie, is sent to the API/off endpoint. Based on the validity of the response, the user may either be redirected to login or to the requested endpoint.
Take note of the config variable with the key “matcher”. It signifies the protected routes of the application, specifically anything following “/app”. If a user attempts to access these routes, their session’s validity is checked. Invalid sessions are redirected to login. Routes not containing “/app” in the URL are accessible to all, ideal for landing pages while reserving certain app sections for logged-in users only.
You can read more about how matcher works here.
5. Design the Login Page:
Using the GoogleAuthProvider, create a login page at src/app/login/page.tsx
. Here, users can log in using their Google accounts.
"use client";
import { getRedirectResult, signInWithRedirect } from "firebase/auth";
import { useEffect, useState } from "react";
import { useRouter } from "next/navigation";
import { auth,provider } from "@/lib/firebase-config";
export default function SignIn() {
const router = useRouter();
useEffect(() => {
getRedirectResult(auth).then(async (userCred) => {
if (!userCred) {
return;
}
fetch("/api/auth", {
method: "POST",
headers: {
Authorization: `Bearer ${await userCred.user.getIdToken()}`,
},
}).then((response) => {
if (response.status === 200) {
router.push("/app");
}
});
});
}, []);
function signIn() {
signInWithRedirect(auth, provider);
}
return (
<>
<h2 className="text-3xl uppercase mb-8">Login page</h2>
<button className="p-4 rounded-lg bg-green-200" onClick={() => signIn()}>Sign In With Google</button>
</>
);
}
At the top of the file, note the inclusion of “use client”. This ensures that the component is executed on the front end in Next.js. If you plan to utilize React hooks like useState, useEffect, or useRef, this line is essential.
6. Fetch User Details:
In src/lib/getUser.ts
, a helper function checks the session's validity and fetches the user's details.
import { cookies } from "next/headers";
import { adminAuth } from "@/lib/firebase-admin-config";
customInitApp()
//Get the user from the session cookie
//if theres no session or its invalid, return null
const getUser = async ()=> {
const session = cookies().get("session")?.value;
if (!session) {
return null;
}
const user = await adminAuth.verifySessionCookie(session, true);
return user;
}
export default getUser
7. Create the Protected App Page:
The src/app/app/page.tsx
page is only accessible to authenticated users. It shows the user's details, and if someone tries to access it without being authenticated, they'll be redirected to the login page.
import getUser from '@/lib/getUser'
import React from 'react'
async function App() {
const user = await getUser()
const formattedString = JSON.stringify(user, null, "\t");
return (
<div className='mx-auto max-w-4xl my-32 '>
<h1 className='text-3xl mb-8'>You are now in protected area of the app</h1>
<p className='mb-4 font-light text-xl'>Here is your information:</p>
<div className="relative bg-gray-800 p-4 rounded-md shadow-md overflow-x-auto">
<pre className="text-sm text-white font-mono">
<code>{formattedString}</code>
</pre>
</div>
</div>
)
}
export default App
A few things about getUser(). It calls our helper function to decode the session cookie and authenticate against the server, determining which user is accessing the app. Pay attention to the ‘async function.’ This is a server component function introduced in Next.js 13 that supports asynchronous components, allowing the use of ‘await’. This function can run only with server components.
8. Logout — BONUS
To log out a user, we can utilize server actions. Wrap the button in a form and pass a handler function to the action. We will reuse the same endpoint (api/auth
) but will need to add a DELETE handler. To log out a user, we will:
- Delete the cookie on the front end (browser).
- Revoke the refresh token on Firebase.
Unfortunately, it’s not possible to handle everything in the server component as one might expect, and you still need to manage such logic on the API route. First, create a Logout.tsx component.
import React from "react";
import { cookies } from "next/headers";
import { redirect } from "next/navigation";
function Logout() {
async function logOut() {
"use server";
const token = cookies().get("session");
await fetch("http:/localhost:3000/api/auth", {
method: "DELETE",
headers: {
Cookie: `session=${token?.value}`,
},
});
cookies().delete("session");
redirect("/login");
}
return (
<form action={logOut}>
<button className="text-white hover:cursor-pointer" type="submit">
Logout
</button>
</form>
);
}
export default Logout;
Secondly, add the following handler to api/auth
.
export async function DELETE(request: NextRequest, response: NextResponse) {
const token = cookies().get("session")?.value || "";
if (!token) {
return NextResponse.json({ isLogged: false }, { status: 401 });
}
await invalidateLogin(token);
return NextResponse.json({}, { status: 200 });
}
// Create a separate file for this utility function if you prefer that way
export const invalidateLogin = async (token: string) => {
const decodedClaims = await getAuth(adminApp).verifySessionCookie(
token,
true
);
await getAuth(adminApp).revokeRefreshTokens(decodedClaims.uid);
cookies().delete("session");
return;
};
This will revoke the refresh token so it can request new access tokens.
Conclusion:
By integrating Firebase with Next.js 13, developers can efficiently handle user authentication on both the front end and back end, ensuring secure and seamless user experiences.
Thank you for reading, Matija
Inspired by
- https://dev.to/geiel/how-to-use-firebase-authentication-in-nextjs-13-client-and-server-side-1bbn
- https://github.com/joeschoe/next-firebase-auth/blob/main/lib/firebase-admin.js
Stackademic
Thank you for reading until the end. Before you go:
- Please consider clapping and following the writer! 👏
- Follow us on Twitter(X), LinkedIn, and YouTube.
- Visit Stackademic.com to find out more about how we are democratizing free programming education around the world.