Secure your Next.js application with Firebase AppCheck

Firebase AppCheck helps us protect our websites against bad actors such as automated bots. In this post, we integrate Firebase AppCheck in a Next.js application.

7 min read
Cover Image for Secure your Next.js application with Firebase AppCheck

Firebase AppCheck is a service provided by Firebase to protect websites against bad actors, such as spammy users or automated bots.

Under the hood, Firebase AppCheck uses Google Recaptcha v3, an automated version of the popular quizzes that can detect bad actors without the need of completing a quiz. In fact, Recaptcha v3 works in the background, and never interrupts the user.

The service is free and helps us protect Firebase Authentication, Firebase Storage, Firebase Firestore and any custom API against threats and disallowed usage of your service.

Obviously, this all sounds great and will surely help you sleep better at night. With that said, you should remember to take into account the implications of using this library, both in terms of bundle size and for your users' privacy. This is why Makerkit disables Firebase AppCheck by default but allows you to easily opt in.

Registering your website for Recaptcha v3

To register your website for Recaptcha v3, you need to complete the linked form. After finishing entering the details, you'll be provided with a Site Key which we use to initialize the Firebase AppCheck service.

NB: when registering the website, make sure to create a Recaptcha v3 key, not v2.

After submitting the form, you will receive two keys:

  1. a public key (on the client-side)
  2. a private key (to be used on the server side)

Save the public key and add it to your Next.js .env file:

NEXT_PUBLIC_APPCHECK_KEY=<YOUR_KEY>

Enabling Firebase App Check from the console

First, we need to enable Firebase App Check from the Firebase Console to register our application using the keys we have generated in the previous step.

Above, you should have added the secret key that we have generated. If it all went well, you have successfully enabled app-check for your application 馃帀!

Enforcing Firestore to use App Check

To enforce Firestore to always validate requests using AppCheck, you will have to enable it from the Console. From where you are, click on the API tab, then click on Firebase Firestore. You will then be presented with the popup as in the image below:

Continue and enforce Firestore to use AppCheck.

Adding Debug Tokens during development

Debug Tokens allow us to use App Check while working in a local development environment or while running automated tests in a CI.

Debug Tokens work well when you're running a Firebase project with real credentials. Instead, if you're running a development project with a demo- prefix, it will not work. These projects require little to no configuration, so they're ideal to get started, but not for testing Firebase's real services.

To create a Firebase Debug Token, click on the registered application menu, and you should then see the window as in the image below:

Once created, copy the debug token and add it as an environment variable to your application:

NEXT_PUBLIC_APPCHECK_DEBUG_TOKEN=<YOUR_DEBUG_TOKEN>

Adding Firebase AppCheck to your Next.js App

It's now time to add the Firebase AppCheck library to our Next.js application, so we can protect it from abuse. To initialize Firebase AppCheck we will use reactfire, the official Firebase library for React applications.

Assuming you already created a Next.js application, ensure to install Reactfire and initialize your Firebase application.

Creating the FirebaseAppCheckProvider component

Then, we create a component named FirebaseAppCheckProvider, responsible for:

  1. retrieving the site key constant appCheckSiteKey from the application's configuration
  2. initializing the AppCheck service using the Firebase method initializeAppCheck
  3. Providing the provider AppCheckProvider

Something very important to remember is to initialize this component before accessing any service, such as Firebase Auth or Firebase Firestore, otherwise, the requests to these services will fail.

import { AppCheckProvider, useFirebaseApp } from 'reactfire';
import { initializeAppCheck, ReCaptchaV3Provider } from 'firebase/app-check';
import configuration from '~/configuration';

const FirebaseAppCheckProvider: React.FCC = ({ children }) => {
  const siteKey = 
    process.env.NEXT_PUBLIC_APPCHECK_KEY;

  const app = useFirebaseApp();

   if (!siteKey || !isBrowser() || configuration.emulator) {
    return <>{children}</>;
  }

  if (!configuration.production) {
    attachAppCheckDebugToken();
  }

  const provider = 
    new ReCaptchaV3Provider(siteKey);

  const sdk = initializeAppCheck(app, {
    provider,
    isTokenAutoRefreshEnabled: true,
  });

  return <AppCheckProvider sdk={sdk}>{children}</AppCheckProvider>;
};

export default FirebaseAppCheckProvider;

To attach a debug token, we need to define the following function:

function attachAppCheckDebugToken() {
  const token =   
    process.env.NEXT_PUBLIC_APPCHECK_DEBUG_TOKEN;

  Object.assign(window, {
    FIREBASE_APPCHECK_DEBUG_TOKEN: token,
  });
}

Using the FirebaseAppCheckProvider component

To use this provider, import it and place it below the FirebaseApp provider. For example, we assume you're initializing Firebase in _app.tsx.

Below is an example of how the Makerkit SaaS starter renders the main application component:

<FirebaseAppShell config={firebase}>
  <FirebaseAppCheckProvider>
    <FirebaseAuthProvider>
      <FirebaseAnalyticsProvider>
        <Component {...pageProps} />
      </FirebaseAnalyticsProvider>
    </FirebaseAuthProvider>
  </FirebaseAppCheckProvider>
</FirebaseAppShell>

If everything went well, AppCheck will now be working on your local Firebase web application.

NB: while running the emulators using a demonstration project (such as demo-makerkit) AppCheck will not be working.

Protecting Next.js API endpoints with Firebase AppCheck

Did you know that we can use Firebase AppCheck to protect a custom backend like the Next.js API endpoints? Well, yes, and it's also fairly simple.

Here is how we do it:

  1. First, we need to generate a token with AppCheck that we will send as a header to our API requests.
  2. The API endpoint is responsible for checking the validity of the token before handling the API handler. If invalid, the request is rejected.

Sending App Check token along your API requests

The implementation to send a token along your API request may differ depending on how you request your API endpoints, but we will show a fairly simple example so that you can adapt it to your own implementation.

Below is a simple React hook to request a Firebase AppCheck token that we will send along as a header.

It's important to notice that we will not use the hook useAppCheck from reactfire, because of a few drawbacks:

  • it throws an error when not initialized
  • we cannot call it conditionally

Therefore, we initialize the SDK manually through its context AppCheckSdkContext, so that we can control its behavior: in our case, we want to fetch the token only when the SDK is provided.

function useGetAppCheckToken() {
  // instead of using useAppCheck()
  // we manually request the SDK
  // because we *may not have initialized it*
  const sdk = useContext(AppCheckSdkContext);

  return useCallback(async () => {
    try {
      // if the SDK does not exist, we cannot generate a token
      if (!sdk) {
        return;
      }

      const forceRefresh = false;

      const { token } =
         await getToken(sdk, forceRefresh);

      return token;
    } catch (e) {
      return;
    }
  }, [sdk]);
}

Now, let's make a simple fetch implementation that takes the token and sends it along. the below is extremely simplified, so please don't copy it literally.

All you need to know is that we need to send the result of the getAppCheckToken promise to be included in the headers of our API request.

async function useFetch<Resp = unknown>(
  url: string,
  payload: string,
  method = 'POST',
  headers?: StringObject
) {
  const options: RequestInit = {
    method,
    headers: {
      accept: 'application/json',
      'Content-Type': 'application/json',
      ...(headers ?? {}),
    },
  };

  const getAppCheckToken = useGetAppCheckToken();
  const token = await getAppCheckToken();

  // if the app-check token was found
  // we add the header to the API request
  if (token) {
    headers['X-Firebase-AppCheck'] = token;
  }

  try {
    const response = await fetch(url, options);

    if (response.ok) {
      return (await response.json()) as Promise<Resp>;
    }

    return Promise.reject(response.statusText);
  } catch (e) {
    return Promise.reject(e);
  }
}

Adding a middleware to protect API routes with AppCheck

To do so, we can create a simple API middleware that checks if the request is successful. If not, it will reject the request before the API handler is executed.

Below is our middleware:

const FIREBASE_APPCHECK_HEADER =
   'X-Firebase-AppCheck';

export async function withAppCheck(
  req: NextApiRequest, 
  res: NextApiResponse
) {
  const appCheck = getAppCheck();
  const token =
     req.headers[FIREBASE_APPCHECK_HEADER];

  const forbidden = () => forbiddenException(res);

  if (!token || typeof token !== 'string') {
    return forbidden();
  }

  try {
    await appCheck.verifyToken(token);
  } catch (e) {
    return forbidden();
  }
}

How to use it?

1) Simply use it within your handlers

export default function(req, res) {
  await withAppCheck(req, res);
}

2) Use it as a chain of middlewares

export default withMiddleware(
  withAdmin,
  withAppCheck,
  withAuthedUser,
  (req, res) => {
    // write here your API handler
  }
)

Conclusion

Many developers are very worried about using Firebase due to security reasons. AppCheck is one of the ways we can protect our applications against abuse, spammy users and automated bots.

Hopefully, this guide will help you improve the security of your Firebase applications and make you sleep better at night.

If you're looking for a complete example of the code above, consider purchasing a copy of our Next.js and Firebase SaaS starter, which will help you get started in no time!


Stay informed with our latest resources for building a SaaS

Subscribe to our newsletter to receive updatesor

Read more about

Cover Image for How to sell code with Lemon Squeezy and Github

How to sell code with Lemon Squeezy and Github

7 min read
Sell and monetize your code by giving private access to your Github repositories using Lemon Squeezy
Cover Image for Writing clean React

Writing clean React

9 min read
Learn how to write clean React code using Typescript with this guide.
Cover Image for How to use MeiliSearch with React

How to use MeiliSearch with React

12 min read
Learn how to use MeiliSearch in your React application with this guide. We will use Meiliseach to add a search engine for our blog posts
Cover Image for Setting environment variables in Remix

Setting environment variables in Remix

3 min read
Learn how to set environment variables in Remix and how to ensure that they are available in the client-side code.
Cover Image for Programmatic Authentication with Supabase and Cypress

Programmatic Authentication with Supabase and Cypress

3 min read
Testing code that requires users to be signed in can be tricky. In this post, we show you how to sign in programmatically with Supabase Authentication to improve the speed of your Cypress tests and increase their reliability.
Cover Image for Reset the Supabase Database in Cypress

Reset the Supabase Database in Cypress

4 min read
Resetting your database during E2E tests is important to prevent flakiness. In this tutorial, we'll show you how to reset the Supabase database in Cypress E2E tests.