Handling authentication persistence in React (with Redux and Firebase) - reactjs

In my React project, I've implemented authentication using Firebase. The workflow is as follows - The UID which is received upon signing in with Google OAuth is used to query the users in the firestore, and accordingly, the user is fetched. This user is then updated to the redux state by the appropriate handler functions. This is the implementation of the sign-in process. The setUser function does the task of updating the redux state
googleAuth = async () => {
firebase.auth().signInWithPopup(provider).then(async (res) => {
const uid = res.user.uid;
const userRef = firestore.collection('users').where('uid', '==', uid);
const userSnap = await userRef.get();
if(!userSnap.empty) {
const user = userSnap.docs[0].data();
this.props.setUser(user);
}
this.props.history.push('/');
}).catch(err => {
alert(err.message);
})
}
The presence of "currentUser" field in the user state of redux, as a result of the aforementioned setUser function, is the basis of opening the protected routes and displaying relevant user-related details in my project. This currentUser is persisted in the state using Redux Persist, so as long as I don't sign out, the localStorage has the "currentUser" object stored, thus meaning that I am signed in.
This implementation, however, causes a problem, that if I update the localStorage using the console and set the currentUser without actually logging in using my Google credentials, the app recognises me as logged in, and I am able to browse all protected routes. Thus, if someone copies some other user's details from his device's localStorage, then he/she can easily access their account and change/delete important information.
What is the correct way to implement authentication in React then, assuming that I do want login state to persist until the user does not sign out manually

Related

no way to catch Firebase: Error (auth/internal-error)

I'm using React js with firebase auth to authenticate user on a web app.
using createUserWithEmailAndPassword and signInWithEmailAndPassword to create/sign-in user, and use onAuthStateChanged to get the current user.
let's say user has created account/signed in and then refreshes browser page while connected to internet, in this case as soon as the app runs, user gets subscribed again by onAuthStateChanged which is in a useEffect hook, until now everything is ok, the problem comes when the user has create account and goes offline and then refreshes the page, so now app throws this error:
I tried to catch error like below code, but no result:
useEffect(() => {
let unsubscribe
try{
unsubscribe = firebaseApp.auth().onAuthStateChanged( userInfo => {
if(userInfo){
const {uid:userId,email,displayName}=userInfo.multiFactor.user
dispatch( setUser({userId,email,displayName}))
}
})
}catch(err){
console.log(err);
setError('You may are offline or have bad internet')
}
return unsubscribe
}, [])
I also tried the way that firebase doc has suggested for errors like below, but yet no result:
useEffect(() => {
const unsubscribe = firebaseApp.auth().onAuthStateChanged( userInfo => {
if(userInfo){
const {uid:userId,email,displayName}=userInfo.multiFactor.user
dispatch( setUser({userId,email,displayName}))
}
},err=>console.log(err))
return unsubscribe
}, [])
I've read firebase doc about (auth/internal-error) but it was not clear on how to catch and handle it.
Any help?
The onAuthStateChanged observer is primarily used to notify the observer code when a user becomes signed in or signed out for any reason. It typically does not deal with errors.
If you want to know if the sign-in process failed, you should check for errors in the code that initiated the sign-in, which you aren't showing here. Somewhere, you must be calling one of the sign-in methods, such as signInWithEmailAndPassword, which does generate errors. Or perhaps you are using FirebaseUI Auth to manage sign-ins, which has its own way of dealing with errors.
However, as you can see in the API documentation for onAuthStateChanged, you can pass an optional second argument which is a function that yields an error, but that is atypical to use, and it's not clear from the API docs how it works. I suggest sticking to handling errors in the code that performs the sign-in as you see in the code in the documentation.

How to redirect an user to a specific webpage after Google Sign In with Redirect in Firebase?

The nature of the sign-in flow with Google/Facebook is that after we login in the redirected page of Google, it comes back to our website's sign-in page.
The following code runs when the Google/Facebook login button is clicked:
fire.auth().signInWithRedirect(provider);
So, my current approach is that I check the Firebase user object using the onAuthStateChanged() function. If the user state is populated, I render a component, else if it is null, I render the component.
{user ? (
<Home />
) : (
<Signup />
)}
But the problem is that after logging in using Google or Facebook, the component is showing for some time (maybe 1-2 secs) and then rendering the component.
I want to render the component immediately after I login using Google redirect. What should I do?
google and facebook login system are asynchronous in nature so you shoud you async await method inside you code.
You should show a full loading state in the login screen when clicking on the signin button
So when the authentication starts show the user a loader,
and if fails stop the loader
you could do something like
if (authUser === undefined || isLoading) {
return <AuthLoader />;
}
return <LoginComponentContents/>
I would recommend to use for all authentication methods the onAuthStateChanged listener in auth. That way it doesn't matter what method you use. It will give you the user if someon is logged in and null if not. The code looks like this:
firebase.auth().onAuthStateChanged(function(user) {
if (user) {
// User is signed in.
} else {
// No user is signed in.
}
});
Use that listener to define the state of your auth (if a user is signed in or not). You can also persiste the data in your state menegement to awoid flickering on page reloads. You can use the same method for email/password login. That way you have a single and simple solution for all Firebasse authentication methods.
For the time you await the Google or other provider redirect login I would recommend to set a loading flag and show a circular loading indicator. The login could always fail. I would not recommend to show pages that require a signed in user untill you get the response from authStateChanged as expected.
For Firebase v9 and above:
If using an AuthContext file, you can define your signInWithRedirect and getRedirectResult functions. Then, import those functions into your Signup page.
AuthContext:
//Log in user with provider. Redirects away from your page.
async function loginGoogle() {
await signInWithRedirect(auth, provider);
return
}
//Checks for provider login result, then navigates
async function getRedirect(){
const result = await getRedirectResult(auth);
if (result) {
navigate('/')
}
}
In your Signup page, call the login function on button click
//Sign in with Google.
async function handleGoogleLogin(e) {
e.preventDefault()
try {
setError('')
await loginGoogle()
} catch(error) {
setError('Failed to log in')
console.log(error)
}
}
Then, just place the getRedirect() function in your Signup component. This function will run when the page reloads from the google redirect, thus sending your user to the desired page.
//Checks for Google Login result.
getRedirect();
For me, it only worked when using this 2-step approach because when the provider redirect occurs, the async function appears to not finished as expected. So loginWithRedirect in one step, then getRedirectResult and navigate in a second step.

How can I stay the user in the same page?

Every time I reload the my account page, it will go to the log in page for a while and will directed to the Logged in Homepage. How can I stay on the same even after refreshing the page?
I'm just practicing reactjs and I think this is the code that's causing this redirecting to log-in then to home
//if the currentUser is signed in in the application
export const getCurrentUser = () => {
return new Promise((resolve, reject) => {
const unsubscribe = auth.onAuthStateChanged(userAuth => {
unsubscribe();
resolve(userAuth); //this tell us if the user is signed in with the application or not
}, reject);
})
};
.....
import {useEffect} from 'react';
import { useSelector } from 'react-redux';
const mapState = ({ user }) => ({
currentUser: user.currentUser
});
//custom hook
const useAuth = props => {
//get that value, if the current user is null, meaning the user is not logged in
// if they want to access the page, they need to be redirected in a way to log in
const { currentUser } = useSelector(mapState);
useEffect(() => {
//checks if the current user is null
if(!currentUser){
//redirect the user to the log in page
//we have access to history because of withRoute in withAuth.js
props.history.push('/login');
}
// eslint-disable-next-line react-hooks/exhaustive-deps
},[currentUser]); //whenever currentUser changes, it will run this code
return currentUser;
};
export default useAuth;
You can make use of local storage as previously mentioned in the comments:
When user logs in
localStorage.setItem('currentUserLogged', true);
And before if(!currentUser)
var currentUser = localStorage.getItem('currentUserLogged');
Please have a look into the following example
Otherwise I recommend you to take a look into Redux Subscribers where you can persist states like so:
store.subscribe(() => {
// store state
})
There are two ways through which you can authenticate your application by using local storage.
The first one is :
set a token value in local storage at the time of logging into your application
localStorage.setItem("auth_token", "any random generated token or any value");
you can use the componentDidMount() method. This method runs on the mounting of any component. you can check here if the value stored in local storage is present or not if it is present it means the user is logged in and can access your page and if not you can redirect the user to the login page.
componentDidMount = () => { if(!localStorage.getItem("auth_token")){ // redirect to login page } }
The second option to authenticate your application by making guards. You can create auth-guards and integrate those guards on your routes. Those guards will check the requirement before rendering each route. It will make your code clean and you do not need to put auth check on every component like the first option.
There are many other ways too for eg. if you are using redux you can use Persist storage or redux store for storing the value but more secure and easy to use.

How to wait for auth state to load on the client before fetching data

Recently I stumbled across the useAuth and useRequireAuth hooks: https://usehooks.com/useRequireAuth/. They are incredibly useful when it comes to client-side authentication. However, what's the correct way for waiting until auth data is available to fetch some other data? I've come up with the following code:
const Page = () => {
// makes sure user is authenticated but asynchronously, redirects if not authenticated, short screen-flash
useRequireAuth()
// actual user object in state, will be updated when firebase auth state changes
const user = useStoreState((state) => state.user.user);
if (!user) {
return <div>Loading</div>
}
useEffect(() => {
if (user) {
fetchSomeDataThatNeedsAuth();
}
}, [user]);
return (
<h1>Username is: {user.name}</h1>
)
}
Is this a "good" way to do it or can this be improved somehow? It feels very verbose and needs to be repeated for every component that needs auth.
This looks fine to me. The thing you could improve is that your useRequireAuth() could return the user, but that's up to you.
Additionally, you probably should check if user is defined before rendering user.name.

Local storage vs. redux state

I currently have an application using redux and I have it set that every time the app loads it checks the JWT token is valid and adds the user data to the state.
I was wondering what the differences are between calling the api and then storing data in the state every reload or storing the data once in localStorage?
How the code is setup with calling the api and storing with redux.
CHECK TOKEN
const token = localStorage.UserIdToken;
if (token) {
const decodedToken = jwtDecode(token);
if (decodedToken.exp * 1000 < Date.now()) {
store.dispatch(logoutUser());
} else {
store.dispatch({ type: SET_AUTHENTICATED });
axios.defaults.headers.common['Authorization'] = token;
store.dispatch(getUserData());
}
}
getUserData()
export const getUserData = () => async dispatch => {
try {
const res = await axios.get('/user');
dispatch({
type: SET_USER,
payload: res.data,
});
}
...
};
First of all, storing the data once in localStorage means that the user will not receive updates to his data, but will always receive the same. Think of seeing the same social media feed every time you log in, which would be the case if it would be saved in localStorage and not requested from the api every reload.
Second, storing data in localStorage instead of the redux state means you use the benefits of using state and redux - redux ensures that components will rerender when state they depend on changes. This ensures that components are responsive to user actions. localStorage won't do that.
Following your comment, I think there is another reason you should consider:
Using localStorage might pose problems if you want to change the user data (add a field for instance). If the data was in 1 place, you could change all user data and let users pull the new data on the next reload. If the data is in localStorage, you will need to add code to your app that will change the existing data on first reload, and then do nothing on other times. This is not a good pattern, and has a better chance of having bugs and problems.

Resources