I am implementing Firebase using Typescript in Next.js15, and in order to improve the security of access to Firestore and Storage, in addition to implementing security rules, I am also enabling Firebase App Check using reCAPTCHA v3 to receive only verified requests.
The initialization of the client-side Firebase App Check for access to Firestore and Storage has been verified without any problems. Following to the Firebase documentation, I am trying to initialize the FirebaseServerApp to call SSR using the AppCheck token obtained at initialization, but I cannot initialize it at all and cannot access Firestore or Storage with SSR.
How can I complete the initialization of Firebase AppCheck in the Next.js SSR environment and verify requests without any problems?
/lib/firebase.ts:
import { getApps, initializeApp } from "firebase/app";
import { getFirestore } from "firebase/firestore";
import { getStorage } from "firebase/storage";
const firebaseConfig = {
apiKey: process.env.NEXT_PUBLIC_FIREBASE_APIKEY,
authDomain: "xxxxxxxxxxxxxxxxxxxx.firebaseapp.com",
projectId: "xxxxxxxxxxxxxxxxxxxx",
storageBucket: "xxxxxxxxxxxxxxxxxxxx.firebasestorage.app",
messagingSenderId: "0000000000",
appId: "0:00000000000:web:00000000000000000000",
measurementId: "G-XXXXXXXXX"
};
const app = !getApps().length ? initializeApp(firebaseConfig) : getApps()[0];
const firestore = getFirestore();
const storage = getStorage();
export { firestore, storage, app };
/firebase_initializer.tsx:
/* Initialize FirebaseAppCheck by placing it in the root layout */
'use client';
import { app } from '@/app/lib/firebase/firebase';
import { getToken } from 'firebase/app-check';
import { useEffect } from 'react';
const FirebaseInitializer = () => {
useEffect(() => {
const initializeAppCheckWithRetry = async () => {
if (typeof window !== "undefined") {
import("firebase/app-check").then(async (firebaseAppCheck) => {
const captachp = process.env.NEXT_PUBLIC_RECAPTCHA_SITE_KEY as string;
const appcheck = firebaseAppCheck.initializeAppCheck(app, {
provider: new firebaseAppCheck.ReCaptchaV3Provider(captachp),
isTokenAutoRefreshEnabled: true,
});
getToken(appcheck, true).then((token) => {
document.cookie = `firebaseAppCheck=${token}`;
});
});
}
};
initializeAppCheckWithRetry();
}, []);
return null;
}
export default FirebaseInitializer;
blog/page.tsx:
Also, somehow, there is no property called appCheckToken in FirebaseServerAppSettings. Therefore, we are using the Any type as a temporary solution.
import { fetchBlogImageList } from '@/app/lib/actions';
import BlogDetail from '@/app/ui/blog_detail/blog_detail';
import { BlogCategory } from '@/app/utils/enum/blog_category';
import { Blog } from '@/app/utils/type/blog';
import { FirebaseServerAppSettings, initializeServerApp } from 'firebase/app';
import { doc, getDoc, getFirestore, Timestamp } from 'firebase/firestore';
import { cookies } from 'next/headers';
import React from 'react'
const Page = async({ params }: { params: Promise<{ id: string }> }) => {
const firebaseConfig = {
apiKey: process.env.NEXT_PUBLIC_FIREBASE_APIKEY,
authDomain: "xxxxxxxxxxxxxxxxxxxx.firebaseapp.com",
projectId: "xxxxxxxxxxxxxxxxxxxx",
storageBucket: "xxxxxxxxxxxxxxxxxxxx.firebasestorage.app",
messagingSenderId: "0000000000",
appId: "0:00000000000:web:00000000000000000000",
measurementId: "G-XXXXXXXXX"
};
const appCheckToken = (await cookies()).get("firebaseAppCheck")?.value;
if (!appCheckToken) {
throw new Error("App Check token is missing in SSR.");
}
const firebaseServerAppSettings : any = {
appCheckToken : appCheckToken
}
const serverApp = initializeServerApp(firebaseConfig, firebaseServerAppSettings)
const firestore = getFirestore(serverApp);
const ref = doc(firestore, "blog", id);
const docSnapshot = await getDoc(ref);
const data = docSnapshot.data();
const blogData : Blog = {
blog_id : data!!.blogId,
created_at : (data!!.createdAt as Timestamp).toMillis(),
title : data!!.title,
category : data!!.category as BlogCategory,
thumbnail : data!!.thumbnail,
messages : data!!.messages,
youtube : data!!.youtube
}
const imagesList = await fetchBlogImageList(id, blogData.messages.map(message => message.images));
return (
<main className='w-full h-full items-center justify-items-center'>
<BlogDetail blog={blogData} blogImages={imagesList}/>
</main>
)
}
export default Page;
