React JS How to handle authentication - javascript

I am trying to join Backend (Express JS) and Frontend (React JS). Right now I am trying to understand how to handle session management.
When a user logins using a form made with React JS, the backend returns a HS-256 JWT token with the user information as a payload.
To avoid unlogged users from fetching data I read that the frontend has to send the bearer token as header and this works fine. But I am not able to find a way of how to avoid the users of using the application in first place, I mean, when every page loads.
Right now my Login code looks like this:
import React, { useEffect, useState } from 'react';
import { useCookies } from 'react-cookie';
export default () => {
const [cookies, setCookie] = useCookies(['auth']);
const [redirect, setRedirect] = useState(false);
const handleSubmit = async e => {
e.preventDefault();
const response = await fetch('server/auth/login', options);
const {token} = await response.json();
setCookie('token', token, { path: '/' });
};
useEffect(() => {
if (cookies.auth) {
setRedirect(true);
}
}, [cookies]);
if (redirect) {
return <Redirect to="/admin" />;
}
return <form onSubmit={handleSubmit}>...</form>
};
I don't know how to use this information in each component to proper set user restrictions. What should I do in order to achieve this?

What you'd want to do is initially check if the by user is authenticated by checking for a the token in local storage / cookie. If so, allow the user to proceed to the route they have visited, if not redirect them back to the login screen ( or error screen). Typically, React users use routing libraries such as react-router-dom. On the documentation of the site, they go over how to go about authentication. Happy coding https://reacttraining.com/react-router/web/example/auth-workflow

Just store the token in local storage and in your router use protected wrappers to routes. If the token doesn't exist or is expired, return redirect to Login.js.
The best way to handle it would be to use an identity and Auth provider like Auth0 and read their documentation for react applications. it's really easy to set up and achieve what you are trying to do out of the box.
Here's an example from my application:
function PrivateRoute({ component: Component, ...rest }) {
return (
<AuthContext.Consumer>
{auth => (
<Route
{...rest}
render={props => {
if (!auth.isAuthenticated()) return auth.login();
return <Component auth={auth} {...props} />;
}}
/>
)}
</AuthContext.Consumer>
);
}

You have to save the token in local storage, so that whenever the user refreshes the page, the token would still exists.
const handleSubmit = async e => {
e.preventDefault();
const response = await fetch('server/auth/login', options);
const {token} = await response.json();
setCookie('token', token, { path: '/' });
localStorage.setItem('token', token);
};
// Check if token exists
const token = localStorage.getItem('token');
if (token && token !== '') {
return <Redirect to="/admin" />;
}
return <form onSubmit={handleSubmit}>...</form>
Note that you still have to validate that token on your server-side script before actually allowing the user to access your admin page.

Related

How to keep token in Redux state after page reload?

so I'm using Redux-Toolkit Query on my project, and I have an authSlice, where I keep the authenticated user info and an access_token.
I also keep this info in local storage so whenever I reload the page I can get the values from the local storage and save them in the state.
The catch is that I have a RequiredAuth component that checks if the user trying to access specific routes is authenticated, by checking if there is an access_token in the state, it works fine except that if I reload this page while I'm authenticated I will be redirected to my login page.
The RequiredAuth component code:
import { useLocation, Navigate, Outlet } from "react-router-dom";
import { useSelector } from "react-redux";
import { selectToken } from "./authSlice";
const RequireAuth = () => {
const token = useSelector(selectToken)
const location = useLocation()
return (
token ? <Outlet /> : <Navigate to="/auth/login" state={{ from:
location}} replace />
)
}
export default RequireAuth
Code that gets user info and token from local storage when the page is reloaded and adds it to state:
import { useEffect, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { setCredentials, selectToken } from '../features/auth/authSlice';
const Header = () => {
const dispatch = useDispatch()
const [user, setUser] = useState(JSON.parse(localStorage.getItem('profile')))
const stateToken = useSelector(selectToken)
useEffect(() => {
if(user?.access_token && !stateToken) {
dispatch(setCredentials({ user: user.user, access_token: user.access_token }))
}
}, [])
// Omited code, not relevant
return (
<header className='nav-bar'>
// Omited code, not relevant
</header>
)
}
export default Header
I believe whenever I reload a page where a user needs to be authenticated this happens: in the "RequiredAuth" I will get a null access_token from the state, so I get redirected and only then my useEffect will copy the local storage data to the state.
I fixed this problem by changing the RequiredAuth component to this:
import { useLocation, Navigate, Outlet } from "react-router-dom";
const RequireAuth = () => {
const profile = JSON.parse(localStorage.getItem('profile'))
const location = useLocation()
return (
profile?.access_token ? <Outlet /> : <Navigate to="/auth/login" state={{ from:
location}} replace />
)
}
export default RequireAuth
But I would like to know if there is a better way to keep data in state after reloading a page in order to solve this problem because getting it from local storage feels counterintuitive, since the data will be stored in the state after the useEffect logic completes.
I think this redux-persist package can help. It basically stores your state inside localStorage/sessionStorage and it will rehidrate your state every time you refresh the page.
You can't keep data with a refresh. Netherless, you can use redux-persist. This package keeps the data inside the localstorage and populate automatically the store on a refresh.
you have to use redux-persist with rtk for the store token. above method, if u r navigating to other routes it will work fine. suppose u r refresh the page u will redirect to the login page
Refer redux-persist

using JWTs saved in localstorage with react context

Im having a really weird issue with My react authentication context (authProvider) and properly rendering my application page. Currently, my root app looks like this:
const App = () => {
const authCtx = useContext(AuthState);
return(
<AuthStateProvider>
<BrowserRouter>
<Switch>
{!authCtx.isLoggedIn && <Route path="/admin" component={SignInUp} />}
{authCtx.isLoggedIn && <Route path="/admin" component={Admin} />}
<Redirect from="/" to="/admin/myDashboard" />
</Switch>
</BrowserRouter>
</AuthStateProvider>
)};
Then in a seperate file that I use to manage the authentication context, I attempt to pull a JWT from local storage and verify it. If that's successful then the context is updated using state variables (including the "isLoggedIn" variable you see above").
const AuthState = React.createContext({
userName: "",
isLoggedIn: false,
authToken: null,
});
const AuthStateProvider = (props) => {
let token = null;
if(localStorage.getItem("token")) return token = localStorage.getItem("token");
const [ user, setUser ] = useState({
userName: "Anonymous",
isLoggedIn: false,
authToken: token,
});
const autoLogin = useCallback( async () => {
try {
const response = await axios({
method:'post',
url: 'http://127.0.0.1:3001/authEn'
headers: {
"Content-Type": "application/json",
"Authentication": user.authToken
}
});
if(response.status === 200){
//code to update context using setUser state handler
} else {
throw new Error("request failed");
}
} catch (e) {
console.log(e.message);
}
});
useEffect( async () => {
await autoLogin();
}, [autoLogin]);
return (
<AuthState.Provider
value={{
userName: user.userName,
isLoggedIn: user.isLoggedIn,
authToken: user.authToken
}}
>
{props.children}
</AuthState.Provider>
);
}
(I've excluded the code for my setUser handler to try and keep this short.)
So the problem is that as of right now, I'm just trying to see that the application can
A: check for stored token on initial page load / reload
B: Navigate you to either logIn or Admin page accordingly.
The app has no problem taking you to logIn page if there is a faulty/no JWT in localstorage. But when I try testing if the application can properly navigate to the admin page when there is a valid token in local storage (i have a seperate helper function to save a valid token), the page loads, but with NONE of the actual admin dashboard. Instead, all there is on the page is the token itself displayed at the top of the window as if it were just an html page with a single div containing the token as a string. I have no Idea why this happens. When I try rendering the admin component (removing the "isLoggedIn" logic and the authStateProvider) everything is fine. But each time I try adding authentication this way things start getting weird. Am I missing something obvious (usually the case)? Is this just a really stupid approach (also usually the case)? Or is this a low-level react issue (I'm not super familiar with all the intricacies of how react works under the hood.)
I think this is a bad practice to make conditions in the Switch.
Instead, you can create a separate component like ProtectedRoute or wrap your pages with a Higher-Order Component (HOC)
First way with ProtectedRoute
Pass isProtected in props if your wrapped route requires authentification
// ProtectedRoute.js
import React, { useContext } from 'react';
import { Redirect } from 'react-router-dom';
const ProtectedRoute = ({
isProtected,
children,
}) => {
const { isLoggedIn } = useContext(AuthState);
if (isProtected && !isLoggedIn) {
return <Redirect to='/login' />;
}
return children
};
export default ProtectedRoute;
Then in your switch:
<Switch>
{ /* Other routes */ }
<ProtectedRoute isProtected>
<Route path="/admin" component={Admin} />
</ProtectedRoute>
</Switch>
HOC
// withAuth
import React, { useContext } from 'react';
import { Redirect } from 'react-router-dom';
const withAuth = (WrappedComponent: any) =>
function (props: any) {
const { isLoggedIn ) = useContext(AuthState);
if (!isLoggedIn) {
<Redirect to='/login' />
}
return <WrappedComponent {...props} />;
}
export default withAuth;
Now you can insert your route in the switch without conditions. If your component requires authentification, wrap it with withAuth.
Example:
const NeedAuth = () => (
<div>Hello I need auth</div>
);
export default withAuth(NeedAuth)
I figured out the issue, and yes it was something super small. In the line of code where I check to see if there is a token stored on localstorage, I use an if block with a return statement. I saw a while back that doing this allows for "if" statements to be written completely on a single line and without brackets {} encapsulating the code. At the time it was really just a style choice but now I see that when the if statement runs (i.e. there is a token in local storage) the return statement within overrides the return statement of the whole functional component. So rather than having a context file that returns a provider that wraps your desired children (my admin page router), It just prints the authtoken. So I returned to traditional styling for the If statement and removed the return statement and it worked fine!

useEffect infinite loop when trying to check if user is logged in

Im using auth0, and on when the app gets re rendered maybe on a page refresh, im trying to do a couple of things with auth0, 1st is check if the user is authenticated, and if he is save his token and user object in the mobx store else redirect him to the login page.
this is the code im currently using:
// appLoaded --> to check if the user is logged in (app cannot be used without a user)
const App: React.FC = () => {
const {
// isAuthenticated,
user,
getIdTokenClaims,
loginWithRedirect,
isAuthenticated
} = useAuth0();
const {
sideNavStore: { isOpen, toggleSideBar },
authStore: { setAppLoaded, setToken, setUser, appLoaded }
} = useStore();
// get the user from auth0 and store their details in the mobx store;
useEffect(() => {
if (isAuthenticated) {
getIdTokenClaims().then((response) => {
// eslint-disable-next-line no-underscore-dangle
setToken(response.__raw);
setUser(user);
});
} else {
loginWithRedirect().then(() => {
setAppLoaded(false);
});
}
}, [
isAuthenticated,
setToken,
setUser,
setAppLoaded,
loginWithRedirect,
getIdTokenClaims,
user
]);
if (!appLoaded) return <PreLoader />;
return (
// some component code
)}
i run into an infinite loop in the code above. i have been on this issue for a day now and just decided to ask on here. could anyone please tell me what im doing wrong? thanks
You shouldn't update any state that a useEffect depends on from within it. That creates an infinite loop.
You have too many dependencies for single useEffect and you're updating some of them. Split your code into multiple useEffect with smaller number of dependencies, ensuring that each of them aren't updating it's own dependency

How to return API (fetched) data from Formik Submit in Next.js

I'm new #frontend and found it a bit challenging creating a simple search form in Next.js with using Formik and return data fetched from my own API back on page. As there are not so much relevant information about, or there is, but it doesn't cover the whole process, so I decided to create this question.
What am I dealing with:
separated front-end and back-end based API on express (both on
localhost right now)
React (+ dom) & Next.js
React Material-UI
Formik
Architecture:
Front send request (based on form input) -> Server received it (operates with it) and respond with json object -> Front takes it, and shows result back on page.
Backend (correct CORS enabled)
A simple node.js express server with one-route (localhost:port/users/:id) when you request it like ./users/100 it returns json with {id}+1 increment, like {id:101}, example code below:
var express = require('express');
var router = express.Router();
/* CORS ARE FINE */
router.get('/:id', function(req, res) {
res.json({id: parseInt(req.params.id)+1});
});
module.exports = router;
Frontend (next.js + formik)
I have already write MyForm component and insert it on my Home page. Here is a code of MyForm component:
import React from 'react';
import fetch from 'isomorphic-unfetch'
import { Formik, Form, Field } from 'formik';
import { TextField } from 'formik-material-ui';
import Button from "#material-ui/core/Button";
const MyForm = () => (
<Formik
initialValues={{ text: '', password: '' }}
onSubmit={ async (values, { setSubmitting }) => {
const res = await fetch(`http://localhost:8000/users/${values.text}`, {
method: 'GET',
mode: 'cors',
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*'
}}).then(response => response.json());
console.log(res);
//props = res;
//return props;
setTimeout(() => {
alert(JSON.stringify(res, null, 2));
setSubmitting(false);
}, 400);
}}
>
{({ isSubmitting }) => (
<Form>
<Field
name="text"
label="Text"
type="text"
variant="outlined"
component={TextField}
/>
<Button
type="submit"
variant="contained"
color="primary"
disabled={isSubmitting}
>
Submit
</Button>
</Form>
)}
</Formik>
);
export default MyForm
As for now, myForm works fine, necessary data received from API via submitting form. The result is stored as res variable and showed as a popup on the page.
But I want to see it, back on page. Like this:
There are not so much tutorials in the web that cover this process step-by-step. I know that in Next.js fetched data received via getInitialProps. (But there are no examples with form submitting)
I place my component on page:
export default function Index() {
return (
<Container maxWidth="sm">
<Box my={4}>
<Form />
<Here is an element with search form results />
</Box>
</Container>
);
}
And then I read that I can't return props from child component back to parent. Or can I? I also can't place res variable as a component value inside or outside my form:
So what I should do now? Anyway I haven't find any relevant example in Formik tutorial. I understand that it is somehow connected with state manipulation like componentDiDMount and that's why frontend devs using modules like Redux. But can someone explain me, what am I doing wrong?
Also, if you have any relevant information and want to give me some advice or share it (like Formik is useless in this case, you should use X instead) don't be shy, just post it. I'll check it out and upvote.
According to #Shyam answer I updated my code. As for now, form has the following code:
const MyForm = (props) => (
<Formik>
...
onSubmit={ async (values, { setSubmitting }) => {
const res = await fetch(`http://localhost:8000/users/${values.text}`, {
method: 'GET',
mode: 'cors',
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*'
}}).then(response => response.json());
console.log(res);
setSubmitting(false);
props.myCallbackFunction(res);
}}
...
and index.js according the below recommendation,
In this case React return me an error, connected with props but the form submission works fine. Guess I could found relevant information here:
https://reacttricks.com/sharing-global-data-in-next-with-custom-app-and-usecontext-hook/
One way would be to send a callback function to MyForm as a prop and have it called with res after the fetched result is received.
export default function Index() {
const [result, setResult] = useState(null)
const callMeOnRes = (data) => {
setResult(data)
}
return (
<Container maxWidth="sm">
<Box my={4}>
<Form myCallbackFunction={(res) => callMeOnRes(res)} />
{result && <div>{result}</div>}
<Here is an element with search form results />
</Box>
</Container>
);
}
Inside MyForm, after the console log you call props.myCallbackFunction(res).
The best way would be to decouple the api call and the frontend logic and using some state management for loading, error, called states. Ideally you should'nt be using setTimeouts in such scenarios.
Hope this helped.

How can i intercept a component to check for permission

I have lots of static forms which i show the user when he clicks on the main menu and goes to a specific route, what i want to do now is to check when going to a route if that component has permission to be visited, i can do this by doing a simple post to server but i am confused and i don't know where should be the place to do this check or post.
Here are some of the solutions i thought of:
1- Writing a Higher order component and wrapping each static component with it
2- creating a base class and making each static form to inherit it while doing this check in the parent class
3- Or maybe using the routes as a solution since i am using the react-router ?
I will appreciate any help or tips.
Thanks.
Create a custom hook like so:-
const useAdmin = (url:string) => {
const [admin, setAdmin] = React.useState(false);
React.useEffect(() => {
post(url, {some: body}).then(res => {
setAdmin(res.admin);
}).catch(err => {
setAdmin(false);
});
}, [])
return [admin];
}
Then use it anywhere:-
const mycomponent = props => {
const [admin] = useAdmin('https://foobar.com/checkPermission');
//Rest of the logic according to the variable 'admin'
return (
<div>
{
admin? <div/>:null
}
</div>
)
}
Or think of admin as permission. Pass it some url for different routes and it will handle it.
I do something similar using react-router as well. I made my own route component that wraps around react-router's route that checks permissions and conditionally renders the route or redirects.
If you're doing the api call each time, then it would look something close to this.
class AppRoute extends Component {
state = {
validCredentials: null
}
componentDidMount() {
// api call here + response
if (ok) {
this.setState({validCredentials: true})
} else {
this.setState({ validCredentials: false})
}
}
render() {
const { validCredentials } = this.state
if (validCredentials) {
return <Route {...this.props} />
} else if (validCredentials === false) {
return <Redirect to="somewhere"/>
}
return null
}
}
You can definitely accomplish this using a Higher Order Component. Just set a state for the user on login like "admin" or "basic_user." According to this state some buttons or forms are going to be available for the user to access. You can also save these access permissions to your backend and call it in the HOC whenever the user logs in.

Categories