1

Essentially, I have a higher order component (HoC), that takes routes, and determines whether or not the currently authenticated user can access the component passed in via props.

Im trying to have a useEffect in my HoC that checks the ID token result of a user, and extracts the custom claims on the user, which were created for the user on the server side at the time of creation using the firebaseAdmin SDK, the custom claims are just the following:

{trial: true, isSubscribed: false} 

Next it stores the value of these custom claims in the state for the component and uses a series of if else statements to determine whether the user is authenticated, and whether or not the user is subscribed if the route trying to be accessed is one requiring a subscription.

Lastly in the return method of my HoC, I render the component conditionally using a ternary operator.

 return (
        <>
        {isAuthenticated? (
            props.component
        ) : (
          <Navigate to='/' />
        )}
        
        </>
    )

Here is my app.js where I pass in the protected routes into my Hoc which is ProtectedPage as a component prop, I also specify whether or not the route going into ProtectedPage requires a subscription or not using the isSubcribedRoute prop:

//App.js

import "./styles/bootstrap.css";
import Home from './components/Home'
import Signup from './components/Signup';
import Login from './components/Login'
import LandingPage from './components/LandingPage'
import Account from './components/Account/Account';
import UserDetails from './components/Account/UserDetails';
import Quizzes from './components/Quizzes';
import Lessons from './components/Lessons';
import Learn from './components/Learn/Learn';
import LearnCourses from './components/Learn/LearnCourses';

import Features from './components/Features'

import ProtectedPage from './components/ProtectedPage';


import { FbMethodContextProvider } from "./firebase/fbMethodContext";


import { createBrowserRouter, RouterProvider} from 'react-router-dom';



import { AuthContextProvider, } from "./firebase/authContext";






const router = createBrowserRouter([
  {
   path: '/',
   element: <Home />,
   children: [
    {
      index: true, 
      element: <LandingPage />

    },
   

    {
      path: '/signup',
      element: <Signup />
    },
    {
      path: '/login', 
      element: <Login />
    },
    {
      path:'/features',
      element: <ProtectedPage component={<Features />} isSubscribedRoute={false } />
    }
   ]
  },
  {
    path: '/account',
    element: <ProtectedPage component={ <Account />} isSubscribedRoute={true}/>, 
    children:[
      {
        index: true,
        element: <UserDetails/>,
      }
    ]
  },
  {
    path: '/quizzes', 
    element: <Quizzes />, 
  },
  {
    path: '/lessons',
    element: <Lessons />

  },
  {
    path: '/learn',
    element: <ProtectedPage component ={<Learn />} isSubscribedRoute={false}/>, 
    children:[
      {
     
     index: true,
      element: <LearnCourses />
      }
    ]
  }

]);

function App() {
  return (
    
      <FbMethodContextProvider>
        <AuthContextProvider>
      <RouterProvider router={router}/>
      </AuthContextProvider>
      </FbMethodContextProvider>
      
   
   
   

   
    
  );
}

export default App;

Next this is my protected page component:


//ProtectedPage.js

import React,{useEffect, useState} from 'react';
import { onAuthStateChanged } from 'firebase/auth';
import {auth} from '../firebase/firebaseConfig';
import {redirect , useNavigate, Navigate} from 'react-router-dom';


const ProtectedPage = (props) => {
    
    const [user, setUser] = useState(null)
    const [isAuthenticated, setIsAuthenticated] = useState(false)
    const [trial, setTrial] = useState()
    const [isSubscribed, setIsSubscribed] = useState()
    const isSubscribedRoute = props.isSubscribedRoute
    const navigate = useNavigate();

    useEffect(()=>{
         onAuthStateChanged(auth, async (user) =>{
            
            if(user){

                const idTokenResult = await user.getIdTokenResult(true);
                


               
                    const onTrial  = idTokenResult.claims.trial;
              
                    setTrial(onTrial);
                    console.log(trial);

                    const Subscribed = idTokenResult.claims.subscribed;
                 
                    setIsSubscribed(Subscribed)

                    console.log(isSubscribed)

                    console.log(`Trial is ${onTrial} & Subscribed is ${isSubscribed}`)
            

                    setUser(user);
                   if(trial || isSubscribed) {
                    setIsAuthenticated(true);
                   }
              

            } else {
                setUser(user)

            }
        })

        
        let isAuthorized;

      

          
        if ( isSubscribedRoute) {
           

            if(isSubscribed){
                isAuthorized = true;
                console.log('user is subscribed and can access this page')
            } else if (trial && isSubscribed === false){
              
                 navigate('/login')
            }
        }
        

           if (!isAuthenticated){
            console.log('not authenticated')

            navigate('/login')
            //The code works when I replace this call to the navigate hook above with react-router-dom's redirect, and replace the <Navigate /> component in the render method below with <div></div>
             

           } else if(!isSubscribedRoute && trial){
            isAuthorized = true;
         

            

           }else if(!isAuthorized){
            console.log('not authorized')
             navigate('/login')
           }
        

        
          

    } )



    return (
        <>
        {isAuthenticated? (
            props.component
        ) : (
          <Navigate to='/' />
        )}
        
        </>
    )
}

export default ProtectedPage

Essentially whenever a user logs in or signsup it should navigate them to the learn page. It all worked fine before I tried to control access with custom claims. Now the only way I can get it to work is by replacing the navigate with redirect('/') in the if(!isAuthenticated) code block and replacing the <Navigate =to '/' /> with in the return function at the bottom.

Any help will be appreciated, or if any better methods or patterns are available please let me know as Im quite new to coding and don't know if my method is even good.

1 Answers1

0

You should consider creating a hook that uses a global context where you handle all checks for the current logged in user. (Here is a guide to set it up)

Inside this hook you can have different states depending on if the user is on trial or subscribed. This way you have direct access when redirecting via routes and don't need to do the check every time.

Right now you fetch the token on each mount but you do the check for isAuthenticated before you've fetched the token. If you use global context you will fetch token one time (the first time you enter the site) and it will be saved between routes.

This will probably resolve your problems.

Edit: To make sure you are making comparisons once the fetch is complete you could use a loading state:

Initialize a boolean state to true: const [loading, setLoading] = useState(true)

Right after const idTokenResult = await user.getIdTokenResult(true); set the loading state to false:

const idTokenResult = await user.getIdTokenResult(true);
setLoading(false)

Then change your return to:

return (
        <>
        {loading? (
            <span>Loading...</span>
        ) : isAuthenticated? (
            props.component
        ) : (
          <Navigate to='/' />
        )}
        
        </>
    )
kingkong.js
  • 659
  • 4
  • 14
  • okay thank you, could you explain how im doing the check for isAuthenticated before though? as i tried using context before and still had the same issue, is it possible for me to fetch the token and delay the isAuthenticated check until after the token has been checked – troy.builds Feb 04 '23 at 19:31
  • Updated my answer. This will work without a context. But I recommend you use context anyway, since it will only fetch the token once (and at each onAuthStateChange) instead of each render – kingkong.js Feb 04 '23 at 19:41
  • okay thanks ill try the loading state method with context and see what happens – troy.builds Feb 04 '23 at 19:41
  • If it doesnt work, try setting the setLoading(false) furthest down in the onAuthStateChanged function – kingkong.js Feb 04 '23 at 19:43
  • I tried and there is still no difference, only time I've ever got it to work correctly was when I use redirect instead of navigate, and the
    instead of navigate component, which I don't understand why
    – troy.builds Feb 04 '23 at 19:48
  • Im tempted to just set the trial and subscribed values for users in the mongoDB database on creation of users, and reading the values into a context hook or redux store instead, but if there are any other solutions to get this firebase custom claims method to work I would greatly appreciate it – troy.builds Feb 04 '23 at 19:52
  • Yes, you should defenitly do that. How do you even store this information right now? – kingkong.js Feb 04 '23 at 19:59
  • when you set custom claims on a user in the backend using firebase admin sdk, they get attached to the users ID token, under the claims property – troy.builds Feb 04 '23 at 20:02
  • Alright, yeah change it to a property in your collection – kingkong.js Feb 04 '23 at 20:58