Authenticated server-side rendering with Next.js and Firebase

Colin McDonnell @vriad

published August 9th, 2020

I had a hell of a time figuring out how to get Firebase Auth, Next.js, tokens, cookies, and getServerSideProps to play nice together. I'm hoping this breakdown will spare you the same suffering!

An example repo that encapsulates the approach below is available at https://github.com/vriad/next-firebase-ssr πŸ€™

I'm currently building a new app. My goal is to do all data fetching inside getServerSideProps, just like the bad ole days of HTML templating. There are a lot of reasons for this.

  • It reduces the cognitive overhead compared to the classic JAMstack approach. The client-server dichotomy is gone β€” it's just a server! So I can deploy my entire application with a single command.
  • It solves the stale bundle problem. With a fully client-side rendered app, an user may use your application for days or weeks without doing a full page refresh. If the API changes underneath them, they may send API payloads that are no longer supported.
  • It strikes a great balance between dynamism and search engine optimization.
  • The user experience is unbeatable. With good caching and performant fetching, you can swap out the ubiquitous SPA spinner for snappy page loads.

As usual, Dan Abramov says it best:

It seems like the SSR-centric approach to building apps with Next.js is gaining steam fast. So I'm extremely confused why the official Next.js with-firebase-authentication example project doesn't demonstrate how to do it! It only demonstrates how to make authenticated POST requests to an API, not authenticate users inside getServerSideProps.

So below I explain how to use Next.js and Firebase Auth to:

  • sign in users (duh)
  • generate ID tokens
  • store those ID tokens as a cookie
  • auto-refresh the cookie whenever Firebase refreshes the ID token (every hour by default)
  • implement authenticated routes
  • authorize the user in getServerSideProps
  • redirect unauthenticated users to a login page from within getServerSideProps

lets dive in

#1 Configure your Firebase JS SDK

You've probably done this already. Should look something like this:

// firebaseClient.ts

import * as firebase from 'firebase/app';
import 'firebase/auth';

if (typeof window !== 'undefined' && !firebase.apps.length) {
  firebase.initializeApp({
    apiKey: 'APIKEY',
    authDomain: 'myproject-123.firebaseapp.com',
    databaseURL: 'https://myproject-123.firebaseio.com',
    projectId: 'myproject-123',
    storageBucket: 'myproject-123.appspot.com',
    messagingSenderId: '123412341234',
    appId: '1:1234123412341234:web:1234123421342134d',
  });
  firebase.auth().setPersistence(firebase.auth.Auth.Persistence.SESSION);
}

export { firebase };

The firebase.apps.length check is a clever way of preventing Next.js from accidentally re-initalizing your SDK when Next.js hot reloads your application.

#2 Configure your Firebase Admin SDK

You've probably done this already too. Check out the Firebase docs for guidance. It'll look something like this:

// firebaseAdmin.ts

import * as firebaseAdmin from 'firebase-admin';

// get this JSON from the Firebase board
// you can also store the values in environment variables
import serviceAccount from './secret.json'; 

if (!firebaseAdmin.apps.length) {
  firebaseAdmin.initializeApp({
    credential: firebaseAdmin.credential.cert({
      privateKey: serviceAccount.private_key,
      clientEmail: serviceAccount.client_email,
      projectId: serviceAccount.project_id,
    }),
    databaseURL: 'https://YOUR_PROJECT_ID.firebaseio.com',
  });
}

export { firebaseAdmin };

#3 Create a Context and provider

import nookies from 'nookies'

// ...

const AuthContext = createContext<{ user: firebase.User | null }>({
  user: null,
});

export function AuthProvider({ children }: any) {
  const [user, setUser] = useState<firebase.User | null>(null);

  useEffect(() => {
    return firebase.auth().onIdTokenChanged(async (user) => {
      if (!user) {
        setUser(null);
        nookies.set(undefined, 'token', '');
        return;
      }

      const token = await user.getIdToken();
      setUser(user);
      nookies.set(undefined, 'token', token);
    });
  }, []);

  return (
    <AuthContext.Provider value={{ user }}>{
      children
    }</AuthContext.Provider>
  );
}

This is where the magic happens.

First, we create the user variable with useState.

Then we set up our authentication listener with useEffect. I'm using firebase.auth().onIdTokenChanged; if you've never used it, it's identical to onAuthStateChanged but it also fires when the user's ID token is refreshed (which happens hourly by default).

In the onIdTokenChanged callback, we check if the user is still signed in. If they are, we set the user with setUser.

Here's the most important part: we also set a token cookie that contains the user's ID token. (To accomplish this, I'm using the excellent nookies package by the great and powerful @maticzav.) Now all outgoing requests β€” both API requests and page navigations! β€” will contain the user's ID token as a cookie! There's an example of this further down the page.

Why use context?

You may be wondering why you need to use React Context at all. Let's consider a more naive approach to writing this hook.

// DONT DO THIS 

export const useAuth = () => {
  const [user, setUser] = useState<firebase.User | null>(null);

  useEffect(() => {
    return firebase.auth().onIdTokenChanged(async (user) => {
      if (user) {
        setUser(user)
      }else{
        setUser(null)
      }
    });
  }, []);

  return { user };
}

This actually works fine. However it would create a new user state variable and a new listener every time you use the hook. We want to make sure the user variable is the same everywhere throughout our application to avoid difficult-to-debug synchronization issues. We only want a single reference to the currently signed in user that is shared throughout our app.

#4 Add your provider to a custom App

The documentation for setting up a Custom App page with Next.js are at https://nextjs.org/docs/advanced-features/custom-app. It's the ideal place to add "wrapper" code that you want to exist on all other pages β€” like Context providers.

// pages/_app.tsx

import type { AppProps } from 'next/app';
import { AuthProvider } from '../auth';

function App({ Component, pageProps }: AppProps) {
  return (
    <AuthProvider>
        <Component {...pageProps} />
    </AuthProvider>
  );
}
export default App;

#5 Create the useAuth hook

Now that the context and provider is set up, our actual useAuth hook is dead simple:

export const useAuth = () => {
  return useContext(AuthContext);
};

#6 Check the token in getServerSideProps

Below is a complete working example of how to implement an authenticated route. If the user isn't properly signed in (e.g. if the token cookies doesn't exist or if the token verification fails) the user is redirected to the /login page.

Otherwise, you can confidently fetch and return data belonging to the user!

// /pages/authenticated.tsx
import React from 'react';
import nookies from 'nookies';
import { InferGetServerSidePropsType, GetServerSidePropsContext } from 'next';

import { firebaseAdmin } from '../firebaseAdmin';

export const getServerSideProps = async (ctx: GetServerSidePropsContext) => {
  try {
    const cookies = nookies.get(ctx);
    const token = await firebaseAdmin.auth().verifyIdToken(cookies.token);

    // the user is authenticated!
    const { uid, email } = token;

    // FETCH STUFF HERE!! πŸš€

    return {
      props: { message: `Your email is ${email} and your UID is ${uid}.` },
    };
  } catch (err) {
    // either the `token` cookie didn't exist
    // or token verification failed
    // either way: redirect to the login page
    ctx.res.writeHead(302, { Location: '/login' })
    ctx.res.end();

    // `as never` prevents inference issues 
    // with InferGetServerSidePropsType.
    // The props returned here don't matter because we've 
    // already redirected the user.
    return { props: {} as never };
  }
};

export default (
  props: InferGetServerSidePropsType<typeof getServerSideProps>
) => (
  <div>
    <p>{props.message}</p>
  </div>
);

The full solution

If you've made it this far you must think the ideas in this post are pretty useful! Consider retweeting this to get the word out: πŸ™Œ

The full code of this project β€” plus some other goodies! a sign up form! a logout button! β€” is available at https://github.com/vriad/next-firebase-ssr. Clone that repo to hit the ground running. πŸƒπŸ½β€β™‚οΈπŸ’¨

By the way, I'm currently working on an extremely rad library that lets you define your API with TypeScript and auto-generate a strongly typed SDK that you can safely use on the client. I think it could be a gamechanger for full-stack TypeScript/Next.js development. Join the mailing list below to stay updated.

1. Personalize your topics

2. Type your email