Home > Software design >  How can I make React app verify user token regularly?
How can I make React app verify user token regularly?

Time:11-23

I have a React app using Firebase Auth and an Express backend. I have React contexts set up for the user's authentication process and for the loading state of the app. Currently, when a user signs in, the following happens:

  1. The app goes into a loading state
  2. The app sends an API request to the backend to verify the user's token
  3. The backend queries the database and then sets the user's custom claims with their permissions and sends a response with the verified token & claims
  4. The loading state is cleared, and the app becomes useable

The user's routes / nav menu options etc are then determined by the user's permissions according to the backend - i.e, if a user doesn't have permission for a certain area of the site, its routes and nav menu items are not loaded.

My authentication context is as follows:

import { createContext, useContext, useState, useEffect } from "react";
/**
 * auth = getAuth()
 * provider = new GoogleAuthProvider()
 */
import { auth, provider } from "providers/firebase";
import {
  getAuth,
  onAuthStateChanged,
  signInWithPopup,
  signOut as firebaseSignOut
} from "firebase/auth";
import { api } from "providers/axios";
import { useLoading } from "providers/loading";

const UserContext = createContext(null);
export const useAuth = () => useContext(UserContext);

const verifyToken = (token) =>
  api({
    method: "post",
    url: "/user/auth",
    headers: {
      token
    }
  });

const UserProvider = (props) => {
  const [user, setUser] = useState(null);
  const { loading, setLoading } = useLoading();

  const signIn = async () => {
    setLoading(true);
    try {
      const result = await signInWithPopup(auth, provider);
      console.log("auth signInWithPopup", result.user.email);
    } catch (e) {
      setUser(null);
      console.error(e);
      setLoading(false);
    }
  };

  const signOut = async () => {
    let userSigningOut = user;
    try {
      await firebaseSignOut(auth);
      setUser(null);
      console.log("signed out");
    } catch (e) {
      console.error(e);
    } finally {
      return (userSigningOut = null);
    }
  };

  const verifyUser = async (user) => {
    try {
      if (!user) {
        throw "no user";
      }

      const token = await getAuth().currentUser.getIdToken(true);
      if (!token) {
        throw "no token";
      }

      const jwt = await getAuth().currentUser.getIdTokenResult();
      if (!jwt) {
        throw "no jwt";
      }

      const verifyTokenResponse = await verifyToken(token);
      if (verifyTokenResponse.data.role !== jwt.claims.role) {
        throw "role level claims mismatch";
      } else {
        user.verifiedToken = verifyTokenResponse.data;
        console.log(`User ${user.uid} verified`);
        setUser(user);
      }
    } catch (e) {
      signOut();
      console.error(e);
    }
  };

  useEffect(() => {
    const unsubscribe = onAuthStateChanged(auth, async (user) => {
      setLoading(true);
      try {
        if (user) {
          console.log("onAuthStateChanged", user?.email);
          await verifyUser(user);
        } else {
          throw "no user";
        }
      } catch (e) {
        console.error(e);
      } finally {
        setLoading(false);
      }
    });
    return unsubscribe;
  }, []);

  return (
    <UserContext.Provider
      value={{
        signIn,
        signOut,
        user
      }}
    >
      {props.children}
    </UserContext.Provider>
  );
};
export default UserProvider;

The problem is that if the user or their permissions are modified, the changes are not reflected in the app until the user performs a hard refresh.

What I'd like to achieve is for the user's token to be re-verified via the backend upon every page change (or similar) and then if their permissions etc have changed, the app then rerenders reflecting the changes. I think this could be achieved by triggering a rerender of a certain part of UserContext after taking it out of the main function, but I'm not sure how to proceed with that.

CodePudding user response:

After @samthecodingman's comment, I added another state for the user's database entry and have achieved the desired outcome with the following changes to UserProvider:

    useEffect(() => {
        if (user) {
            const userDataRef = ref(db, `/users/${user.uid}`);
            onValue(userDataRef, async snapshot => {
                await verifyUser(user);
                setUserData(snapshot.val());
            })
        }
    }, [user]);

  return (
    <UserContext.Provider
      value={{
        signIn,
        signOut,
        user,
        userData
      }}
    >
      {props.children}
    </UserContext.Provider>
  );
  • Related