Setting Up Protected Routes with Firebase and React

25/06/2022 • 4 min read

While we were working on some projects we had a discussion about handling routes in Firebase. So you will read about my approach in this post. Maybe you will get a new perspective or validate your own approach.

Determining The User Role for Authorization

In Firebase, users are connected to the Authentication module. That means authentication data is not in the application database. Therefore if you want to use the database for the users, you would have to manually handle users relationship with their data.

However, there is a concept called Custom Claims which is a part of The Firebase Admin SDK. Skipping the part where we setup user claims with the Admin SDK since this post is focused on the routing in the web UI.

Firebase reference with fire roof work (Picture by John Odegard)

Providing The User Roles to Your Application

Our project already had a provider setup. But to summarize it, please take a look at this code block:

// AuthProvider returns:
;<AuthContext.Provider
   value={{ user, isAuthenticated: !!user, ...someOtherProps }}
   children={children}
/>
// Export a hook to use it
export const useAuth = () => useContext(AuthContext)

Firebase Client library has a method called onAuthStateChanged. First we get the user with it. Then we have to get the user claims. With the response from onAuthStateChanged, we call getIdTokenResult through it. This provides us with there properties:

  • authTime
  • claims
  • expirationTime
  • issuedAtTime
  • signInProvider
  • signInSecondFactor
  • token

What we needed is claims and we got it. So we will assign the needed fields to our user value. We only needed admin so I did user.isAdmin = idTokenResult.claims.admin. Here is the code:

// Should be initialized once with the provider,
// You can use this in a useEffect
firebase.auth().onAuthStateChanged((user) => {
   if (user && onLogin) {
      user.getIdTokenResult().then((idTokenResult) => {
         user.isAdmin = idTokenResult.claims.admin
         // Or mock it
         // user.isAdmin = true; // TODO with Admin SDK
         onLogin(user)
         setUser(user)
      })
   } else setUser(user)
})

With this, we have can access wheter the user has the admin role by:

const { user } = useAuth()
console.log(user.isAdmin)

Setting Up The Route Component

Our goal is to create something like below:

// For all logged in users
<ProtectedRoute path="/profile" component={UserProfile} />
// For admins only
<ProtectedRoute isAdminRoute path="/admin" component={Admin} />

I had only added the user claim check to our higher-order component for authentication check. And it was done very similar to Auth0's component in their library.

We are following you meme

We start by writing the HoC, but I am going to redact some pieces to make it simpler:

function withAuthenticationRequired(Component, options) {
   return function WithAuthenticationRequired(props) {
      // This will be passed from ProtectedRoute
      const { isAdminRoute = false } = props
      // Use our hook to get the user info
      const { isAuthenticated, user } = useAuth()
      const {
         returnTo = defaultReturnTo, // a function that returns a url path
         onRedirecting = defaultOnRedirecting, // returns a component that displays when redirecting
      } = options

      useEffect(() => {
         let isAuthorized = false

         if (isLoaded) {
            /* If authenticated,
             * Is the route admin only?
             * If route is admin only, is the user */
            isAuthorized = isAdminRoute ? !!user?.admin : isAuthenticated

            const opts = {
               appState: {
                  returnTo: typeof returnTo === "function" ? returnTo() : returnTo,
               },
            }

            if (!isAuthenticated) history.push("/login", opts)
            else if (!isAuthorized) history.push("/", opts) // because already logged in
         }
      }, [history, isAuthenticated, loginOptions, returnTo])

      return isAuthenticated ? <Component {...props} /> : onRedirecting()
   }
}

The ProtectedRoute component, quite simple after what we wrote:

const ProtectedRoute = ({ component, ...args }) => {
   // Wrap the component we get with the HoC we just wrote
   const WrappedComponent = withAuthenticationRequired(component, {
      onRedirecting: () => "Resuming the session…",
   })

   return (
      <Route
         render={(routeProps) => <WrappedComponent {...routeProps} {...args} />}
      />
   )
}

And this matches our goal!

End

I have mentioned some conventions about Firebase, a React provider, a higher-order component and a route component. Hope this helps you. We did this implementation as a part of a Developer Acceleration Program (the actual name is vague at the moment :D) by Scott Coates thanks to him and everyone in it. Here is the GitHub organization connected to it.

"Setting Up Protected Routes with Firebase and React", 25/06/2022, 08:30:00

#firebase, #guide, #javascript, #react, #web-development