Recently I made a 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 . small web app here Authentication - Client Side Initialization Setting up Firebase is easy. You create a project 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. here firebase ; ; ; clientCredentials = { : process.env.NEXT_PUBLIC_FIREBASE_API_KEY, : process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN, : process.env.NEXT_PUBLIC_FIREBASE_DATABASE_URL, : process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID, : process.env.NEXT_PUBLIC_FIREBASE_APP_ID, }; (!firebase.apps.length) { firebase.initializeApp(clientCredentials); } firebase //lib/firebase.js import from 'firebase/app' import 'firebase/auth' import 'firebase/firestore' const apiKey authDomain databaseURL projectId appId if export default (See file and folder structure in the actual project) here 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 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. authContext = createContext(); { auth = ; //lib/auth.js const export ( ) function AuthProvider { children } const /* something we'll fill in later */ return {children} ; } < = > authContext.Provider value {auth} </ > 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 useAuth = { useContext(authContext); }; SomeComponent = { { user, loading } = useAuth(); } //lib/auth.js export const => () return //components/SomeComponent.js const => () const // 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 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 React, { useState, useEffect, useContext, createContext } firebase authContext = createContext() { auth = getAuth() //lib/auth.js import from 'react' import from './firebase' const export ( ) function AuthProvider { children } const return {children} } 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} } < = > authContext.Provider value {auth} </ > authContext.Provider 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 . 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 ). 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. rules here note here Below is a fuller implementation of userFirebaseAuth. There are quite a few things we added here: 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. 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. Adding some formatting to make the user object only contains our app's necessary info and not everything that Firebase returns. Add redirect to send user to the right pages after sign-in or sign-out. React, { useState, useEffect, useContext, createContext } ; Router ; firebase ; { createUser } ; authContext = createContext(); { auth = useFirebaseAuth(); import from 'react' import from 'next/router' import from './firebase' import from './db' const export ( ) function AuthProvider { children } const return {children} ; } 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, }; }; < = > authContext.Provider value {auth} </ > authContext.Provider 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 to enforce access permissions. security rules 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 { doc = firestore.collection( ).doc(uid).get(); user = { : doc.id, ...doc.data() }; user; } export async ( ) function getUser uid const await 'users' const id return But we define the following set of security rules to only allow read/write when the user's uid matches the document's uid. rules_version = ; service cloud.firestore { match /databases/{database}/documents { match /users/{uid} { allow read, : isUser(uid); } } } { isSignedIn() && request.auth.uid == uid; } { request.auth.uid != ; } '2' write if ( ) function isUser uid return ( ) function isSignedIn return 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 I used, which involved a bit of that. security rules 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 for more details.) here 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 ( ) and some on the server-side ( ). I probably should consolidate to the server-side. code pointer code pointer 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 * admin ; (!admin.apps.length) { admin.initializeApp({ : admin.credential.cert({ : process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID, : process.env.FIREBASE_CLIENT_EMAIL, : process.env.FIREBASE_PRIVATE_KEY.replace( , ), }), : process.env.NEXT_PUBLIC_FIREBASE_DATABASE_URL, }); } firestore = admin.firestore(); auth = admin.auth(); { firestore, auth } //lib/firebase-admin.js import as from 'firebase-admin' if credential projectId clientEmail privateKey /\\n/g '\n' databaseURL const const export The documentation 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. here Now we can extract uid on each request and verify the user's access { auth } ; (req, res) => { (!req.headers.token) { res.status( ).json({ : }); } { { uid } = auth.verifyIdToken(req.headers.token); req.uid = uid; } (error) { res.status( ).json({ : error.message }); } } import from '@/lib/firebase-admin' export default async if return 401 error 'Please include id token' try const await catch return 401 error // 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 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 { auth } ; { (req, res) => { authHeader = req.headers.authorization; (!authHeader) { res.status( ).end( ); } token = authHeader.split( )[ ]; decodedToken; { decodedToken = auth.verifyIdToken(token); (!decodedToken || !decodedToken.uid) res.status( ).end( ); req.uid = decodedToken.uid; } (error) { .log(error.errorInfo); errorCode = error.errorInfo.code; error.status = ; (errorCode === ) { error.status = ; } res.status(error.status).json({ : errorCode }); } handler(req, res); }; } import from '@/lib/firebase-admin' export ( ) function withAuth handler return async const if return 401 'Not authenticated. No Auth header' const ' ' 1 let try await if return 401 'Not authenticated' catch console const 401 if 'auth/internal-error' 500 //TODO handlle firebase admin errors in more detail return error return And this is how we can use it, notice instead of exporting handler we are exporting withAuth(handler) { withAuth } ; { getUserSites } ; handler = (req, res) => { { { sites } = getUserSites(req.uid); res.status( ).json({ sites }); } (error) { .log(error); res.status( ).json({ : error.message }); } }; withAuth(handler); // get all sites of a user import from '@/lib/middlewares' import from '@/lib/db-admin' const async try const await return 200 catch console return 500 error export default Here are the relevant files on GitHub: and . middleware.js sites route 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. Previously published at https://www.dingran.me/next-js-authentication/