React Route protection with HOC PrivateRoute component with asynchronous jwt token validation - reactjs
I am not very experienced with React yet and have been trying to approach the problem of securing specific routes from authentication.
For this purpose, I need to implement a HOC component, specifically a "PrivateRoute" component.
Some main generalities:
I'm developing with React 17.0.2 and then react-router-dom 6.2.1
I need to verify the presence of the token on a Redis server (in the cloud) and validate it.
The project is divided into two "modules," one in React for the frontend and one in node js/express, which exposes the server-side logic.
In the frontend module, the routes are implemented as follows:
return (<Router>
<div className="App">
<nav className="navbar navbar-expand-lg navbar-light fixed-top">
<div className="container">
<Link className="navbar-brand" to={"/"}>My Site</Link>
<div className="collapse navbar-collapse" id="navbarTogglerDemo02">
<ul className="navbar-nav ml-auto">
<li className="nav-item">
<Link className="nav-link" to={"/sign-in"}>Login</Link>
</li>
<li className="nav-item">
<Link className="nav-link" to={"/sign-up"}>Sign up</Link>
</li>
</ul>
</div>
</div>
</nav>
<Routes>
<Route exact path='/' element={<PrivateRoute><Home /></PrivateRoute>} />
<Route path="/sign-in" element={<Login />} />
<Route path="/sign-up" element={<SignUp />} />
<Route path="*" Component={<Login/>} status={403}/>
</Routes>
</div></Router>
As you can see, for example, I want the Home component to be protected, and therefore I implement the PrivateRoute component with which I wrap the Home component.
The implementation of the PrivateRoute component must validate any jwt token present in localStorage, passing it to an HTTP validation service exposed by the node/express module. The token is sent to the http service by passing the Authorization Bearer header.
The logic of retrieving from the localStorage and sending to the validation service is implemented with a useEffect (() => {}, []). Inside it is defined async function, so you can make the axios call with await. The function is always called from the body of useEffect. However, I cannot understand why updating the state variable created with useState seems not to update correctly (synchronization problem?). This does not seem to trigger the rendering update if the Home component is validated correctly (in the ternary operator, home is represented by props.children), rather than redirecting to the login page.
Below I report the implementation of the PrivateRoute:
export const PrivateRoute = (props) => {
console.log('props',props);
const [tokenValid, setTokenValid] = useState('');
useEffect(() => {
const checkRedisToken = async () => {
console.log('Access localStorage');
const ni_token = localStorage.getItem('ni_token');
console.log('Returned localStorage');
const config = { headers: { 'Authorization': `Bearer ${ni_token}` } };
console.log('Call axios');
const response = await axios.get('http://localhost:5000/identity/verifytoken', config);
const data = response.data;
console.log('After axios ', data);
setTokenValid(response.data);
}
checkRedisToken();
console.log('tokenValid', tokenValid);
}, []);
return (
tokenValid !== '' ?
props.children :
<Navigate to="/sign-in" />
)
}
Thank you in advance for your help in understanding my mistakes.
Thank you so much
I try to describe the solution I have adopted. The solution is the result of the reading of similar posts previously published and of readings scattered on the web.
As noted by #tromgy the problem of my first implementation is caused by the asynchronous call to the endpoint / identity / verifytoken.
The flow of execution, after the call, continues (waiting for a result) and the tokenValid state variable, even if set, seems not to be immediately detected by React and a new rendering is not invoked.
The correct solution seems to be to add an additional "loadingComplete" state.
The "loadingComplete" state seems to handle the stages where the component is not or is mounted waiting for the authenticated state to be ready and set as well.
The declaration of the routes has remained unchanged and for the sake of simplicity I show the code fragment below:
function App() {
return (<Router>
<div className="App">
<nav className="navbar navbar-expand-lg navbar-light fixed-top">
<div className="container">
<Link className="navbar-brand" to={"/"}>My Site</Link>
<div className="collapse navbar-collapse" id="navbarTogglerDemo02">
<ul className="navbar-nav ml-auto">
<li className="nav-item">
<Link className="nav-link" to={"/sign-in"}>Login</Link>
</li>
<li className="nav-item">
<Link className="nav-link" to={"/sign-up"}>Sign up</Link>
</li>
</ul>
</div>
</div>
</nav>
<Routes>
<Route exact path='/' element={<PrivateRoute><Home /></PrivateRoute>} />
<Route path="/sign-in" element={<Login />} />
<Route path="/sign-up" element={<SignUp />} />
<Route path="*" Component={<Login/>} status={403}/>
</Routes>
</div></Router>
);
}
export default App;
As mentioned, the changes made to the PrivateRoute component are quite substantial. Before giving a brief description below, I report the new implementation below:
export const PrivateRoute = (props) => {
console.log('props', props);
const [authenticated, setAuthenticated] = useState(null);
const [loadingComplete, setLoadingComplete] = useState(false);
useEffect(() => {
const ni_token = localStorage.getItem('ni_token');
const isLogin = async (ni_token) => {
console.log('isLogin');
try {
const config = { headers: { 'Authorization': `Bearer ${ni_token}` } };
const result = await axios.get('http://localhost:5000/identity/verifytoken', config);
setAuthenticated(result.data);
} catch (e) {
console.log(e);
}
setLoadingComplete(true);
}
isLogin(ni_token);
}, []);
if(loadingComplete){
console.log('Completed', authenticated);
return (authenticated === 'Verify Token Respone' ?
props.children :
<Navigate to="/sign-in" />)
}else {
console.log('Loading...');
return (<div> Loading...</div>)
}
}
As you can see the useEffect with empty dependency array, so that the passed function is only executed once.
On the first call to the web application root we observe the execution of console.log ('props', props).
Immediately thereafter, the console.log trace shows ('Loading ...').
Immediately after, the "isLogin" log appears, in fact useEffect calls isLogin (ni_token). At this moment the asynchronous http call is made to the endpoint "/ identity / verifytoken" in charge of validating the token. At the first access to the frontend (http: // localhost: 3000) there is no token and the endpoint returns an http 403 (Forbidden) status. The authenticated state variable is not changed and its value remains "null", but lodingComplete is set to "true" causing <Navigate to = "sign-in /> to be returned, ie the redirect to the login page.
As you can see, the change in state determines the rendering of the component, the "props" log and the "Completed null" log reappear.
Below you can find what is logged at first access:
props Object
Loading ...
Failed to load resource: the server responded with a status of 403 (Forbidden)
props Object
Completed null
Some thougths for further deepening about not so clear aspects: if I do not misinterpret, at the new rendering the stream passes again through console.log ('props', props) and obviously react does not re-execute the useStates? Otherwise loadingComplete would be false again and would render Loading while as expected it goes through redirection to the login component. Is my interpretation correct?
If I finally log in and a positive authentication happens, the flow highlighted with the logs executed with console.log is the following:
props {children: {…}}
Loading ...
isLogin
props {children: {…}}
Loading ...
props {children: {…}}
Completed Verify Token Respone
If I'm not wrong, the re-rendering is done twice in addition to the first one. But there is something I can't quite understand. The useEffect is performed (log isLogin) the asynchronous http call is performed and setAuthenticated will be performed later the flow of execution certainly goes through setLoadingComplete (true) at this point I expect a re-rendering but contrary to what I expect I still see the "Loading" log ... But on the other hand if this is not the case it should still redirect to the Login component. Eventually, the "Completed Verify Token Respone" log appears at the setAuthenticated setting.
I'm still a bit puzzled who helps me understand what I see and what is happening? I hope this post can be useful to learn more about the mechanisms and functioning of React and of this specific need.
Related
How stop the flicker in the SideNav caused by the useEffect which fetches some data on every render - Reactjs
I have a parent component that holds a SideBar component and an that renders out the nested routed components in the parent. The SideBar has useEffect hook to make an API call to retrieve some data such as the name of the user, and if the user is verified which I use to show the name of the user on the top of the Nav and conditionally render buttons. Now because of the nested routing the SideBar re-renders every time. Hence the useEffect hook makes the API call every time which makes the layout flicker because it takes a small time to get the data from the async function. Is there any way to stop that flicker? below is the code SideBar.js const SideBar = (props) => { // selecting slices for getting user data const { authTokens, user } = useSelector((state) => state.login); // setting states const [teacher, setTeacher] = useState(null); //to store the teacher detail const [loading, setLoading] = useState(true); const constant = localStorage.getItem("token") ? true : null; useEffect(() => { // setting the loading true to wait for the data setLoading(true); /* this is the procedure to retrive data from an async api call because we cannot directly use async calls so we have to wrap them inside another small function. */ const fectData = async () => { //await here is neccessary to wait for the promise to get resolved let response = await fetchTeacherDetail(user.user_id); setTeacher(response.data); setLoading(false); }; fectData(); }, [constant]); const profileButton = ( <Fragment> <NavLink activeclassname="acitve" to="/teacher/profile"> <i class="fas fa-user"></i>Profile </NavLink> </Fragment> ); const enterDetails = ( <Fragment> <NavLink activeclassname="acitve" to="/teacher/submit-info"> <i class="fas fa-pen"></i> Enter Details </NavLink> </Fragment> ); return ( <SideNav> <UserNameContainer> <p>{!loading && teacher.name}</p> </UserNameContainer> <ButtonContainer> <Fragment> {!loading && teacher.is_verified ? profileButton : enterDetails} </Fragment> <NavLink activeclassname="acitve" to="info"> <i class="fas fa-info-circle"></i> My Information </NavLink> <NavLink activeclassname="acitve" to="student-requests"> <i class="fas fa-user-plus"></i> Requests </NavLink> <NavLink activeclassname="acitve" to="enrolled-student"> <i class="fas fa-user-graduate"></i> My Students </NavLink> <NavLink activeclassname="acitve" to="payment"> <i class="fas fa-rupee-sign"></i> Payments </NavLink> </ButtonContainer> </SideNav> ); }; export default SideBar; Dashboard.js return ( <Container> <SideBar /> <ContentMainContainer> {/* this makes the nested routes display here */} <Outlet /> </ContentMainContainer> </Container> ); The gif is showing only one flicker perhaps because the chrome capture but it happens for all the links I click. Please suggest me to avoid this flicker.
The component is flickering because the height of the <p> tag containing the name is changing. You can try setting a height for it. The easiest way to do that is by setting the style inline, like this: <p style={{minHeight: '1rem'}}>{!loading && teacher.name}</p> Just remember, every time you remove or add some piece of content, it will possibly affect everything else.
How to route a details page of a photo when clicked in React?
I have a react component which returns some photos that i got from an API. What i want is, when i click to a particular photo, i want to go to a details page ,and not see the other photos around the clicked one.for each different photo, i want to go to the url '/photoId' and i want that url doesn't include anything except the details page. think it like amazon shopping. in the browsing page you see a lot of different products,but when you click them, you go to a details page and don't see other products anymore. you see price, reviews etc in that detail page. that is what i want to do. here is the code that i use for routing right now. characters is an array that i fetched from api. And Detail is the component that takes a character prop and returns details about it. Currently , when i clicked a photo, i see the details but i see other products too. How to fix it? in the normal flow of the page , page looks like this right now:normal flow but when i click to a button, it looks like this:clicked Thanks for help. <ul> {characters.map(x => <li> <Link to={`/${x.id}`} > <div className='profile'> <img className='profileImage' src={x.image} /> <div className='profileName'></div>{x.name} </div> </Link> <Route exact path={`/${x.id}`} component={() => <Detail character={x} />} > </Route> </li>)} </ul>
Here's a working snippet that mimics your additional code sample - the Switch shouldn't actually be necessary but it doesn't work properly without it in Stack Snippets for some reason. I also added in some basic state and paramater validation to demonstrate how to control routing when you can't pass the character data directly from the link, like your were trying to do in your original example. I'd recommend looking over any unfamiliar aspects of the api at the react router docs. const { useState } = React; const { BrowserRouter, Route, Link, Redirect, Switch } = window.ReactRouterDOM; const Detail = ({ name }) => { return <h2>{name}</h2>; }; const App = () => { const [characters] = useState([ { name: "rick" }, { name: "morty" } ]); return ( <BrowserRouter> <Switch> <Route exact path="/"> <Link to="/rick">go to rick</Link> <Link to="/morty">go to morty</Link> <Link to="/summer">go to summer</Link> </Route> <Route exact path="/:id" render={(routeProps) => { const id = routeProps.match.params.id; const exists = characters.find((char) => char.name === id); if (exists) return <Detail name={exists.name} />; return <Redirect to="/" />; }} /> </Switch> </BrowserRouter> ); }; ReactDOM.render(<App />, document.body) <script crossorigin src="https://unpkg.com/react#16/umd/react.production.min.js"></script> <script crossorigin src="https://unpkg.com/react-dom#16/umd/react-dom.production.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/react-router-dom/5.0.1/react-router-dom.min.js"></script>
Different behavior after page is reloaded (app is in production mode, made by react and express)
Everything works fine in development mode. At first look app is identical in build mode (I am using express for server side). But when I try to refresh my page (url contains params) I receive only { surveyString: "[{"type":"radiogroup","name":"Favorite Character?","title":"Favorite Character?","isRequired":true,"colCount":1,"choices":["Walt","Hank","Jesse","Mike"]},{"type":"radiogroup","name":"Favorite Actor?","title":"Favorite Actor?","isRequired":true,"colCount":1,"choices":["Aaron Paul","Bryan Cranston","Jonathan Banks"]}]", surveyTitle: "Breaking Bad Survey" } which is response send from server, which of course i want, but somehow app doesn't incorporate it. Error which i receive in Chrome is Not allowed to load local resource: view-source In Firefox error looks somehow different Content Security Policy: The page’s settings blocked the loading of a resource a ...resource link... Through these errors I acknowledged about Content Security Policy, some basic method how express package works to overcome it and so on... But that is different story and I hope I will solve problem on react side. I am optimist because I have identical behavior from a href links on other page. When I replace aHref with LinkTo app works as excepted. I click on link and on a new page everything is ok, until I make refresh. link is to github page: component which not survives refresh function SingleSurvey(props) { const [complete, setComplete] = useState(false); const [voted, setVoted] = useState(false); const [json, setJson] = useState({}); const [title, setTitle] = useState(""); const [resultsDisplay, setResultsDisplay] = useState(false); const { id } = useParams(); useEffect(() => { surveyServices.getSingle(id).then((res) => { const stringQuestions = JSON.parse(res.surveyString); setJson({ questions: stringQuestions }); setTitle(res.surveyTitle); }); }, [id]); useEffect(() => { const checkItem = window.localStorage.getItem(`chartVoted-${id}`); if (checkItem) { setVoted(true); } // eslint-disable-next-line }, []); function onComplete(result) { surveyServices.updateVotes({ data: result.data, id, }); window.localStorage.setItem(`chartVoted-${id}`, true); setComplete(true); setTimeout(() => { window.location.reload(); }, 1100); } return ( <main className="survey-page"> {complete && <p className="survey-finished"></p>} {!voted ? ( <> <Survey.Survey json={json} showCompletePage={false} onComplete={(result) => onComplete(result)} /> <div className="show-results"> <button className="btn-results" onClick={() => setResultsDisplay(!resultsDisplay)} > {resultsDisplay ? "Hide Charts" : "Show Charts"} </button> <div className="visible-results" style={{ display: resultsDisplay ? "" : "none" }} > <Result id={id} title={title} /> </div> </div> </> ) : ( <div className="just-results"> <Result id={id} title={title} /> </div> )} </main> ); } export default SingleSurvey; switch routes settings <Router> <Notification /> <Switch> <Route exact path="/api/survey/single/:id" component={SingleSurvey} /> </Switch> </Router> ReactDOM render, I am using basic redux <Provider store={store}> <App /> </Provider>, express app.use(express.static(path.join(__dirname, "build"))); app.use("/api/survey", surveyController); app.use("/api/users", usersController); app.use("/api/login", loginController); app.get("/", function (req, res) { app.get("/*", function (req, res) { res.sendFile(path.join(__dirname, "build", "index.html")); }); }); If you want to see directly problem app is uploaded on heroku. I hope I don't miss any valuable information. Anyone has idea how to fix unbelievably annoying problem?
You have confused and consequently overloaded your API routes and your client side routes. When web browsers go to a website they are making GET requests, exactly the same as how you interact with your express server. Your issue here is you are accidentally making a get request to your server by redirecting your users to your server endpoint which makes a GET request to your server. What is happening Url change triggered by a Link component (not in the code you've provided React Router matches the URL without triggering an API request (this is how single page web app work) <Route exact path="/api/survey/single/:id" component={SingleSurvey} /> On page refresh Express matches the URL and returns the API response instead of the index.html app.use("/api/survey", surveyController); Solution Change your client-side paths to something like: /client/survey
Following Alex Mckay's answer I change route location <Route exact path="/client/survey/single/:id" component={SingleSurvey} /> And than fix content security policy error on my server side. It is whole new area for me so I follow errors in my console and fix it one by one. It is highly intuitive process with [helmet-csp] package. app.use( csp({ directives: { defaultSrc: ["'self'"], fontSrc: ["https://fonts.googleapis.com/", "https://fonts.gstatic.com"], connectSrc: ["'self'", "http://localhost:3005"], styleSrc: [ "'self'", "'unsafe-inline'", "https://surveyjs.azureedge.net/1.8.0/modern.css", "https://fonts.googleapis.com", "'sha256-47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU='", "'sha256-OTeu7NEHDo6qutIWo0F2TmYrDhsKWCzrUgGoxxHGJ8o='", ], imgSrc: ["'self'", "http://localhost:3005"], scriptSrc: [ "'self'", "unsafe-inline", "'sha256-eE1k/Cs1U0Li9/ihPPQ7jKIGDvR8fYw65VJw+txfifw='", ], objectSrc: ["'none'"], upgradeInsecureRequests: [], }, reportOnly: false, }) ); Also I removed inline style from one of my component because component behaved strange <> <Survey.Survey json={json} showCompletePage={false} onComplete={(result) => onComplete(result)} /> <div className="show-results"> <button className="btn-results" onClick={() => setResultsDisplay(!resultsDisplay)} > {resultsDisplay ? "Hide Charts" : "Show Charts"} </button> {resultsDisplay && ( <div> <Result id={id} title={title} /> </div> )} </div> </> And it works.
Route path="/subject/:selectedSubject" does not change subject when navigated via < Link />
I'm trying to display articles filtered by subject. When you access directly path="/subject/:selectedSubject" (subject/TECH) for example it works perfectly. However if you navigate through <Link to={"/subject/TECH"} /> it will change the URL but will not load new articles. I've tried: connecting everything with "withRouter". I know that the <Link/> is changing the redux state, however that is not calling componentWillMount() which is where fetchArticles() is. So, my question is, where should I put fetchArticles in order for it to be called when is triggered. I tried putting it inside render() but it keeps getting called non-stop. Or maybe I'm approaching this the wrong way. PS: if another path gets called via <Link/>, like for example path="/", it works as intended (loads up new articles). at App/ <BrowserRouter> <div> <Route path="/" exact component={ArticlesList} /> <Route path="/subject/:selectedSubject" component={ArticlesList} /> </div> </BrowserRouter> at ArticleList/ componentDidMount() { if (this.props.match.params.selectedSubject) { const selectedSubject = this.props.match.params.selectedSubject.toUpperCase(); this.props.fetchArticles(selectedSubject); } else { this.props.fetchArticles(); } } at Link/ {this.props.subjects.map(subject => { return ( <Link to={`/subject/${subject.name}`} key={subject.name}> <li> <span>{subject.name}</span> </li> </Link> ); })}
Since you use the same Component for / and /subject/:selectedSubject, he's not calling mounting lifecycle methods, including constructor() and componentDidMount(). You have to use updating lifecycle method, especially componentDidUpdate() to handle a update of your component.
"Maximum update depth exceeded" in React functional component
I have a problem with a react functional component. When the react-apollo query is "on completed" executes a code to handle a token, and when finish send to another page (using history.push). When run the code I get a infinite loop with this message: Maximum update depth exceeded. This can happen when a component repeatedly calls setState inside componentWillUpdate or componentDidUpdate. React limits the number of nested updates to prevent infinite loops I've tried comment history.push and the infinite loop does not happen again. I comment all the logic in list-session component to avoid a recursive call, but it does not work (I checked thoroughly the code to confirm is not a recursive call). I thing the problem is related with the lifecycle or with the history.push behavior Ptt: I'm learning react on this moment const { setUserInfo, setUserId } = useContext(userContext); const { token } = match.params; const onCompleted = data => { if (data.validateUser.status) { setUserId(token); setUserInfo(data.validateUser.infoUser); localStorage.setItem("token", token); // history.push("/list-session"); } else { history.push("/rare-page"); } }; return ( <div className="Auth" data-testid="AuthPage"> <Query query={VALIDATE_USER} variables={{ userId: token }} onCompleted={onCompleted} > {({ error }) => { if (error) return <Error />; return ( <> <h2 data-testid="AuthState">Authenticating...</h2> <div className="spinner-border text-info" role="status"> <span className="sr-only">Loading...</span> </div> </> ); }} </Query> </div> ); } EDIT 1: Added relevant code in the problem. I checked the component and no one of this make a recursive call, and no make a infinite setState call. The setState problem is the consecuence to another problem. <AnimatedSwitch atEnter={bounceTransition.atEnter} atLeave={bounceTransition.atLeave} atActive={bounceTransition.atActive} mapStyles={mapStyles} className="route-wrapper" > <Route exact path="/" component={NoCredentials} /> <Route exact path="/token/:token" component={Auth} /> <Route path="/list-session" component={Home} /> </AnimatedSwitch> and this is the code in Home component: const Home = () => { const [activeTab, setActiveTab] = useState("activeSessions"); return ( <ValidateToken> <Container className="bg-light"> <div className="Home"> <CreateSessionComponent /> <Nav tabs> <NavItem> <NavLink className={classnames({ active: activeTab === "activeSessions" })} onClick={() => setActiveTab("activeSessions")} > Active Sessions </NavLink> </NavItem> </Nav> <TabContent activeTab={activeTab}> <TabPane tabId="activeSessions"> {activeTab === "activeSessions" && ( <div> <SessionsListComponent status="ACTIVE" /> </div> )} </TabPane> </TabPane> </TabContent> </div> </Container> </ValidateToken> ); }; export default Home;
The "maximum update depth exceeded" error occurs when you are trying to update a variable multiple times until the stack size exceeds. This usually happens when u are trying to set a state inside render() or calling a method inside render which in turn sets the state. So check if there is a setState() call inside render method of your code. Also this might happen because of setting the localStorage variable inside render. Try to move it out of render. But if you anyways need it you can use the componentDidMount() hook. That way all your variables will be available to you when your component is rendered.
try this it may work i think. <Query query={VALIDATE_USER} variables={{ userId: token }} onCompleted={(e)=>onCompleted(e)} //onCompleted throws an event >