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:
- The app goes into a loading state
- The app sends an API request to the backend to verify the user's token
- 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
- 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>
);