Server Side Authentication with Firebase and Next.js

Server-Side Authentication with Firebase Auth and Next.js

In this tutorial, I'll walk you through setting up server-side authentication for an application using Firebase Auth and Next.js. The goal is to enable server-side rendering (SSR) for an app that requires user-specific data, leveraging one of Next.js’s key benefits.

TL;DR: Store the Firebase user idToken as a cookie. Use this cookie in getInitialProps to call an API endpoint that validates the token using the Firebase Admin SDK. If valid, fetch the required data and pass it to the app as props.


The Problem with Firebase Auth

Firebase Auth’s JavaScript SDK is designed for client-side authentication. Here’s a typical flow:

  1. The user logs in, and Firebase writes a cookie accessible only to the Firebase SDK.
  2. On subsequent visits, the Firebase SDK automatically verifies or refreshes the token, seamlessly logging the user in.
  3. The app renders after authentication is complete, sometimes causing a visible UI flash.

For SSR, this approach is insufficient because the server cannot access user-specific data until the Firebase SDK completes its client-side work.


Why Use Server-Side Authentication?

SSR can eliminate UI flashes and delays caused by client-side authentication. If the server can verify the user during the initial page load and fetch the necessary data, it can render a fully populated page to send to the browser. The key to achieving this lies in using cookies to manage user authentication.


Setting Up the User Login Flow

  1. Handle User Login: When a user logs in, write a cookie containing the Firebase idToken, which can be validated server-side. Use the js-cookie library to manage cookies in the browser:

    import cookie from 'js-cookie';
    import firebase from 'firebase/app';
    import 'firebase/auth';
    
    const TOKEN_NAME = 'idToken';
    
    firebase.auth().onAuthStateChanged(async (user) => {
      if (user) {
        const token = await user.getIdToken();
        cookie.set(TOKEN_NAME, token, { expires: 1 });
      } else {
        cookie.remove(TOKEN_NAME);
      }
    });
    
  2. Handle Logout: Remove the cookie when the user logs out to ensure the server no longer authenticates them.


Server-Side Token Validation

On the server, retrieve and validate the token stored in the cookie. Use the Firebase Admin SDK for secure verification.

  1. Set Up the Firebase Admin SDK:

    Create an API route (/api/validate) to validate the token and fetch user data:

    import * as admin from 'firebase-admin';
    
    const adminApp = admin.initializeApp({
      credential: admin.credential.cert({
        // Your Firebase Admin credentials
      }),
    });
    
    export default async (req, res) => {
      try {
        const { token } = JSON.parse(req.headers.authorization || '{}');
        if (!token) {
          return res.status(403).send({ message: 'Auth token missing.' });
        }
    
        const decodedToken = await admin.auth().verifyIdToken(token);
        const user = await admin.auth().getUser(decodedToken.uid);
    
        // Fetch additional user data from Firestore
        const userData = (
          await admin.firestore().doc(`/users/${decodedToken.uid}`).get()
        ).data();
    
        return res.status(200).send({ user, userData });
      } catch (error) {
        return res.status(401).send({ message: error.message });
      }
    };
    
  2. Retrieve and Validate the Cookie in _app.tsx:

    Use next-cookies to access the token and validate it via the API during SSR:

    import App from 'next/app';
    import nextCookie from 'next-cookies';
    import fetch from 'isomorphic-unfetch';
    
    class MyApp extends App {
      static async getInitialProps(appContext) {
        const { ctx } = appContext;
        const appProps = await App.getInitialProps(appContext);
    
        if (typeof window === 'undefined') {
          const { idToken } = nextCookie(ctx);
    
          if (idToken) {
            try {
              const response = await fetch(`${process.env.API_URL}/api/validate`, {
                method: 'POST',
                headers: {
                  'Content-Type': 'application/json',
                  Authorization: JSON.stringify({ token: idToken }),
                },
              });
              const { user, userData } = await response.json();
              return { ...appProps, user, userData };
            } catch {
              // Handle invalid token or API errors
            }
          }
        }
    
        return appProps;
      }
    
      render() {
        const { Component, pageProps, user, userData } = this.props;
        return <Component {...pageProps} user={user} userData={userData} />;
      }
    }
    
    export default MyApp;
    

Key Benefits of Server-Side Authentication

  • Improved User Experience: Avoid UI flashes and deliver fully populated pages on load.
  • Enhanced Security: Validate tokens securely on the server instead of relying solely on client-side logic.
  • Optimized Performance: Pre-render pages with necessary data, reducing client-side API calls.

Wrapping Up

With this setup, your Next.js app can leverage Firebase Auth for secure server-side authentication, enabling SSR for user-specific data. This approach improves user experience, enhances security, and optimizes performance. Happy coding!