Next.js: Firebase Authentication and Middleware for API Routes
Tags: nextjs, react, beginners, javascript
Published: 2021-02-28 03:48:33

> This article was originally published on [my blog](https://www.dingran.me/next-js-authentication/). Head over there if you like this post and want to read others like it.

Recently I made a [small web app](https://ghutils.dingran.me/) that requires user accounts. I learned quite a bit about setting up authentication with Firebase on the client-side and using it on the server-side to protected API routes with a middleware pattern similar to Express.js. This post is a recap of what I learned based on this project for future reference. You can find the code for this project on GitHub [here](https://github.com/dingran/ghost-utils).

Authentication - Client Side
----------------------------

### Initialization

Setting up Firebase is easy. You create a project [here](https://console.firebase.google.com/) and enable the sign-in providers you plan to use, along with authorized domains. Grab the credentials from Project Settings in the Firebase console, and we can initialize the Firebase SDK on the client-side like this.

```javascript
//lib/firebase.js
import firebase from 'firebase/app';
import 'firebase/auth';
import 'firebase/firestore';

const clientCredentials = {
  apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY,
  authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN,
  databaseURL: process.env.NEXT_PUBLIC_FIREBASE_DATABASE_URL,
  projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID,
  appId: process.env.NEXT_PUBLIC_FIREBASE_APP_ID,
};

if (!firebase.apps.length) {
  firebase.initializeApp(clientCredentials);
}

export default firebase;
```

(See file and folder structure [here](https://github.com/dingran/ghost-utils/blob/main/lib/firebase.js) in the actual project)

### React Hooks and Context Provider

Since the user's authentication status is a "global" state, we can avoid recursively passing it as a prop through many layers of components by using [Context](https://reactjs.org/docs/context.html).

To do this, we need a context Provider and a context Consumer. A Provider comes with a Context created by `createContext()`. The `value` prop we pass to the Provider will be accessible by its children.

```javascript
    //lib/auth.js
    const authContext = createContext();
    
    export function AuthProvider({ children }) {
      const auth = /* something we'll fill in later */;
      return <authContext.Provider value={auth}>{children}</authContext.Provider>;
    }

For the descendant components to use the value, i.e., consume the Context, we can use `Context.Consumer`, or more conveniently, the `useContext` [hook](https://reactjs.org/docs/hooks-reference.html#usecontext).

    //lib/auth.js
    export const useAuth = () => {
      return useContext(authContext);
    };
    
    //components/SomeComponent.js
    const SomeComponent = () => {
      const { user, loading } = useAuth();
      // later we can use the object user to determine authentication status
      // ...
      }
```
    

In Next.js, the `AuthProvider` we implemented above can be inserted in the `_app.js` so all the pages in the app can use it. See [here](https://github.com/dingran/ghost-utils/blob/e82e6b5c42ab4e31c804695e2b0cb0665aa7595e/pages/_app.js#L44).

### Implementation Details of `AuthProvider`

In the `AuthProvider` skeleton above, we passed an `auth` object as the `value` prop, and this is the key thing that all the consumers consume. Now we need to figure out what we need to implement this `auth` object.

The key thing `auth` need to achieve is subscribing to the changes in the user's login status (and associated user info). These changes can be triggered through the Firebase SDK, specifically the sign-in / sign-out functions such as `firebase.auth.GoogleAuthProvider()` and authentication state observer function `firebase.auth().onAuthStateChanged()`.

So, our minimal implementation could be the following, mainly pay attention to the new `getAuth` function. We definitely need to return something from `getAuth` and that'll be the `auth` object used by `AuthProvider`. To do this, we implement the `handleUser` function to update the state `user` as follows

```javascript
    //lib/auth.js
    import React, { useState, useEffect, useContext, createContext } from 'react'
    import firebase from './firebase'
    
    const authContext = createContext()
    
    export function AuthProvider({ children }) {
      const auth = getAuth()
      return <authContext.Provider value={auth}>{children}</authContext.Provider>
    }
    
    export const useAuth = () => {
      return useContext(authContext)
    }
    
    function getAuth() {
      const [user, setUser] = useState(null)
      const handleUser = (user) => {
        if(user){
          setUser(user)
        }
      }
    
      useEffect(() => {
        const unsubscribe = firebase.auth().onAuthStateChanged(handleUser);
        return () => unsubscribe();
      }, []);
      
      /* TBA: some log in and log out function that will also call handleUser */
      
      return {user}
    }
```

Since we are calling other React Hooks, e.g. `userEffect`,  `getAuth` needs to be either a React functional component or a custom hook in order to follow the [rules here](https://reactjs.org/docs/hooks-rules.html). Since we are not rendering anything, just returning some info,  `getAuth` is a custom hook and we should thus rename it to something like `useFirebaseAuth` (i.e the custom hook's name should always start with `use`, per [note here](https://reactjs.org/docs/hooks-custom.html)). The main function `userFirebaseAuth` provides us is to share the `user` state between components. Actually, across all the components since we used a `Context` Provider in `_app.js`.

Below is a fuller implementation of `userFirebaseAuth`. There are quite a few things we added here:

1.  Exposing sign-in and sign-out logic so that context consumers can use them. Since they would trigger changes in `user` state similarly to `firebase.auth().onAuthStateChanged`, it is better to put them here.
2.  We actually need to change `firebase.auth().onAuthStateChanged` to `firebase.auth().onIdTokenChanged` to capture the token refresh events and refresh the `user` state accordingly with the new access token.
3.  Adding some formatting to make the `user` object only contains our app's necessary info and not everything that Firebase returns.
4.  Add redirect to send user to the right pages after sign-in or sign-out.

```javascript
    import React, { useState, useEffect, useContext, createContext } from 'react';
    import Router from 'next/router';
    import firebase from './firebase';
    import { createUser } from './db';
    
    const authContext = createContext();
    
    export function AuthProvider({ children }) {
      const auth = useFirebaseAuth();
      return <authContext.Provider value={auth}>{children}</authContext.Provider>;
    }
    
    export const useAuth = () => {
      return useContext(authContext);
    };
    
    function useFirebaseAuth() {
      const [user, setUser] = useState(null);
      const [loading, setLoading] = useState(true);
    
      const handleUser = async (rawUser) => {
        if (rawUser) {
          const user = await formatUser(rawUser);
          const { token, ...userWithoutToken } = user;
    
          createUser(user.uid, userWithoutToken);
          setUser(user);
    
          setLoading(false);
          return user;
        } else {
          setUser(false);
          setLoading(false);
          return false;
        }
      };
      
      const signinWithGoogle = (redirect) => {
        setLoading(true);
        return firebase
          .auth()
          .signInWithPopup(new firebase.auth.GoogleAuthProvider())
          .then((response) => {
            handleUser(response.user);
    
            if (redirect) {
              Router.push(redirect);
            }
          });
      };
    
      const signout = () => {
        return firebase
          .auth()
          .signOut()
          .then(() => handleUser(false));
      };
    
      useEffect(() => {
        const unsubscribe = firebase.auth().onIdTokenChanged(handleUser);
        return () => unsubscribe();
      }, []);
    
      return {
        user,
        loading,
        signinWithGoogle,
        signout,
      };
    }
    
    const formatUser = async (user) => {
      return {
        uid: user.uid,
        email: user.email,
        name: user.displayName,
        provider: user.providerData[0].providerId,
        photoUrl: user.photoURL,
      };
    };
```

Authorization - Server Side
---------------------------

The other use case we need with Firebase authentication is to ensure users have proper access to server-side resources, i.e., specific API routes will be only accessible if certain access criteria is met. I guess this called authorization. An example would be, for `/api/users/[uid]` route, we would only return results the user requesting their own info.

### Firestore Security Rules

One pattern to manage access to backend resources (mostly database access) is to use Firestore and Firebase authentication together and use Firestore's [security rules](https://firebase.google.com/docs/firestore/security/get-started) to enforce access permissions.

For example, in the example above, to limit access to user info, on the client-side, we attempt to retrieve the user record as usual

```javascript
    export async function getUser(uid) {
      const doc = await firestore.collection('users').doc(uid).get();
      const user = { id: doc.id, ...doc.data() };
      return user;
    }
```
But we define the following set of security rules to only allow read/write when the user's `uid` matches the document's `uid`.
```javascript
    rules_version = '2';
    service cloud.firestore {
      match /databases/{database}/documents {
        match /users/{uid} {
          allow read, write: if isUser(uid);
        }
      }
    }
    function isUser(uid) {
      return isSignedIn() && request.auth.uid == uid;
    }
    function isSignedIn() {
      return request.auth.uid != null;
    }
```

You can actually do a lot with this setup. For example, in order to determine access to a document, you can do some extra queries on other collections and documents. Here are the [security rules](https://github.com/dingran/ghost-utils/blob/main/firestore.rules) I used, which involved a bit of that.

With this client-side setup and security rules, there are downsides. Mainly:

*   We are defining access using this security rule syntax, which is less flexible than just writing arbitrary code on the server-side.
*   Firestore also limits the number of queries you can do to verify the access permission on each request. This may limit how complex your permission scheme can be.
*   Some of the database operations can be very heavy, such as recursively deleting a large document collection, and should only be done on the server-side. (See Firestore's documentation [here](https://firebase.google.com/docs/firestore/manage-data/delete-data) for more details.)
*   Testing security rules require extra work. (Firebase does have a friendly UI and simulator for this).
*   Finally, it gets a little scattered that some database access logic lives on the client-side ([code pointer](https://github.com/dingran/ghost-utils/blob/main/lib/db.js)) and some on the server-side ([code pointer](https://github.com/dingran/ghost-utils/blob/main/lib/db-admin.js)). I probably should consolidate to the server-side.

### Using Firebase Admin on Server Side

OK, now the more "classic" way of doing the authorization on the server-side. The general workflow is:

*   The client-side code should send over an access token along with each request.
*   The server-side code has an instance of `firebase-admin`, which can verify and decode the access token and extract user information, such as the `uid` of the user
*   Based on that information, the server-side code can do more queries and apply more logic to figure out it should proceed or reject the request. (The `firebase-admin` will have privileged access to all Firebase resources and will ignore all the security rules, which are only relevant for client-side requests).

This is how I initialized `firebase-admin`

```javascript
    //lib/firebase-admin.js
    
    import * as admin from 'firebase-admin';
    
    if (!admin.apps.length) {
      admin.initializeApp({
        credential: admin.credential.cert({
          projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID,
          clientEmail: process.env.FIREBASE_CLIENT_EMAIL,
          privateKey: process.env.FIREBASE_PRIVATE_KEY.replace(/\\n/g, '\n'),
        }),
        databaseURL: process.env.NEXT_PUBLIC_FIREBASE_DATABASE_URL,
      });
    }
    
    const firestore = admin.firestore();
    const auth = admin.auth();
    
    export { firestore, auth }
```

The documentation [here](https://firebase.google.com/docs/admin/setup) suggests generate a private key JSON file. The file contains many different fields, the three fields above: `projectId`, `clientEmail`, and `privateKey` seem to be enough to get it to work.

Now we can extract `uid` on each request and verify the user's access

```javascript
    import { auth } from '@/lib/firebase-admin';
    
    export default async (req, res) => {
      if (!req.headers.token) {
        return res.status(401).json({ error: 'Please include id token' });
      }
    
      try {
        const { uid } = await auth.verifyIdToken(req.headers.token);
        req.uid = uid;
      } catch (error) {
        return res.status(401).json({ error: error.message });
      }
    
      // more authorization checks based on uid 
      // business logic
    }
```

### Authentication Middleware for Next.js API Routes

One small annoyance with the above is that as we have more API routes that need authentication, the code need to be repeated in these API routes functions. I find Next.js out of the box doesn't have as strong a support for server-side development. A couple of things from Express.js I wish Next.js have are: routers and [middleware](https://expressjs.com/en/guide/using-middleware.html).

In this scenario, making authentication work as a middleware would be convenient. Middleware is things you can plug into the request handling lifecycle; the middleware would enrich the request and/or the response objects and can terminate the request early if errors occur.

It turned out to be pretty straightforward, we just need to create a wrapper for our normal handler function, and in the wrapper we can modify the `req` and `res` objects and return early if errors occur.

Here is how I defined a `withAuth` middleware

```javascript
    import { auth } from '@/lib/firebase-admin';
    
    export function withAuth(handler) {
      return async (req, res) => {
        const authHeader = req.headers.authorization;
        if (!authHeader) {
          return res.status(401).end('Not authenticated. No Auth header');
        }
    
        const token = authHeader.split(' ')[1];
        let decodedToken;
        try {
          decodedToken = await auth.verifyIdToken(token);
          if (!decodedToken || !decodedToken.uid)
            return res.status(401).end('Not authenticated');
          req.uid = decodedToken.uid;
        } catch (error) {
          console.log(error.errorInfo);
          const errorCode = error.errorInfo.code;
          error.status = 401;
          if (errorCode === 'auth/internal-error') {
            error.status = 500;
          }
          //TODO handlle firebase admin errors in more detail
          return res.status(error.status).json({ error: errorCode });
        }
    
        return handler(req, res);
      };
    }
```

And this is how we can use it, notice instead of exporting `handler` we are exporting `withAuth(handler)`

```javascript
    // get all sites of a user
    import { withAuth } from '@/lib/middlewares';
    import { getUserSites } from '@/lib/db-admin';
    
    const handler = async (req, res) => {
      try {
        const { sites } = await getUserSites(req.uid);
        return res.status(200).json({ sites });
      } catch (error) {
        console.log(error);
        return res.status(500).json({ error: error.message });
      }
    };
    
    export default withAuth(handler);
```

Here are the relevant files on GitHub: [middleware.js](https://github.com/dingran/ghost-utils/blob/718b43ad2d50600037953e3538b4e2cb315995c1/lib/middlewares.js) and [sites route](https://github.com/dingran/ghost-utils/blob/718b43ad2d50600037953e3538b4e2cb315995c1/pages/api/auth/sites.js).

* * *

That's all I learned about authentication on the client and server side with Next.js and Firebase. Overall it's a great developer experience and pretty painless to figure things out.

> This article was originally published on [my blog](https://www.dingran.me/next-js-authentication/). Head over there if you like this post and want to read others like it.