Utilizing cookies for authentication requires managing the user's auth state independently rather than relying on the Firebase client SDK. Essentially, this method mirrors the traditional approach of establishing cookie-based authentication, with Firebase handling tokens/cookies and their revocation when necessary.
To implement this, you will need to create an API that accepts the user's Firebase ID Token upon their login through the client SDK and subsequently sets the session cookie.
import "server-only";
import { NextResponse } from "next/server";
import { authAdmin } from "@/utils/firebase-admin";
import { cookies } from "next/headers";
// /api/auth/login
export async function POST(request: Request) {
const authorization = request.headers.get("authorization")?.split(" ")[1];
const sessionCookie = await authAdmin.createSessionCookie(authorization, {
expiresIn: 14 * 24 * 60 * 60 * 1000, // 14 days in ms (maximum)
});
// setting the session cookie
cookies().set("session", sessionCookie, {
httpOnly: true,
secure: true,
domain: "localhost",
});
return NextResponse.json({ message: "Logged in" });
}
It is recommended to configure the Firebase auth persistence as NONE
to ensure users are logged out of the Firebase client SDK later on. Subsequently, you can invoke the previously created API in the following manner:
// Sample login page
"use client";
import { auth } from "../../utils/firebase";
import { createUserWithEmailAndPassword, getIdToken } from "firebase/auth";
import { useState } from "react";
export default function Login() {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const signUpUser = async (event: any) => {
event.preventDefault();
try {
const userCredential = await createUserWithEmailAndPassword(
auth,
email,
password
);
// call POST /api/auth/login with user's token
const token = await getIdToken(userCredential.user);
const response = await fetch("/api/auth/login", {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
});
const data = await response.json();
// redirect user
} catch (error) {
console.error(error);
}
};
return (
<form className="max-w-sm mx-auto" onSubmit={signUpUser}>
<div className="mb-5">
<label>Your email</label>
<input
type="email"
id="email"
onChange={(event) => setEmail(event.target.value)}
value={email}
required
/>
</div>
<div className="mb-5">
<label>Your password</label>
<input
type="password"
id="password"
onChange={(event) => setPassword(event.target.value)}
value={password}
required
/>
</div>
<button type="submit">Sign Up</button>
</form>
);
}
Upon successful login, it is essential to verify the user's auth states using cookies. This validation can be achieved utilizing Next Middlewares like so:
// middleware.ts
import { NextResponse } from "next/server";
import { NextRequest } from "next/server";
import { authAdmin } from "./utils/firebase-admin";
export async function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
if (pathname.startsWith("/_next") || pathname.startsWith("/favicon.ico")) {
return NextResponse.next();
}
const authorization = request.headers.get("authorization")?.split(" ")[1];
if (!authorization) {
return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
}
try {
const { uid } = await authAdmin.verifyIdToken(authorization);
console.log("Current user:", uid)
return response;
} catch (e) {
console.log("failed to decode session cookie")
// Unauthorized user..
}
}
I advise creating an additional API endpoint to retrieve user details, which can then be utilized to display current user information in sections like the navigation bar.
In conclusion, it's crucial to monitor the user's auth state on the server side, preferably within a middleware, to appropriately guide users to login or dashboard pages based on their authentication status.