I am using React-Router V4 and I want to hand over an 'authenticated' prop to decide wether to send the user to a login page or to the requested page:
PrivateRoute.js
import React from "react";
import {
Route,
Redirect,
} from "react-router-dom";
const PrivateRoute = ({ component: Component, authenticated, ...rest }) => (
<Route
{...rest}
render={props =>
authenticated ? (
<Component {...props} />
) : (
<Redirect
to={{
pathname: "/login",
state: { from: props.location }
}}
/>
)
}
/>
)
export default PrivateRoute;
My App.js looks like this:
class App extends Component {
is_authenticated() {
const token = localStorage.getItem('access_token');
//if token is not expired send on the way
if (token && jwt_decode(token).exp > Date.now() / 1000) {
return true;
} else {
// if token is expired try to refresh
const refresh_token = localStorage.getItem('refresh_token');
if(refresh_token) {
axios.post(config.refresh_url,{},{
headers: {"Authorization": `Bearer ${refresh_token}`},
"crossdomain": true,
mode: 'no-cors',}).then(
response => {
const access_token = response.data.access_token;
localStorage.setItem('access_token', access_token)
return access_token ? true : false
}
)
}
}
return false
}
render() {
const client = this.client;
return (
<Router>
<div>
<Route exact path="/login" component={Login} />
<PrivateRoute exact path="/" component={Home} authenticated={this.is_authenticated()} />
</div>
</Router>
);
}
}
export default App;
Since the Axios Call is async the component renders before the call is finished.
How can I make the render wait for the token to be refreshed?
I'd keep isAuthenticated in the App components state. Then, you can use setState in the isAuthenticated call to cause a re-render on the result. This will also be important when the tokens expire.
Related
I'm practicing fundamentals by creating a basic authentication using PERN. My user auth routes are working on the front- and back-end. Logging in and out with JWT functionality works, but when I try to set up a ternary operator that will remember the user's authorization status even when you click to another page, it breaks everything. Here's the code for my client entry point.
import React, { Fragment, useState, useEffect } from "react";
import './App.css';
import { BrowserRouter as Router, Switch, Route, Redirect } from "react-router-dom";
//components
import Dashboard from "./components/Dashboard";
import Login from "./components/Login";
import Register from "./components/Register";
function App() {
async function isAuth() {
try {
const response = await fetch("http://localhost:4000/api/v1/auth/verify/", {
method: "GET",
headers: { jwt_token : localStorage.token }
});
const parseRes = await response.json();
//parseRes === true ? setIsAuthenticated(true) :
//setIsAuthenticated(false);
} catch (err) {
console.error(err.message);
}
}
useEffect(() => {
isAuth()
})
const [isAuthenticated, setIsAuthenticated] = useState(false);
const setAuth = boolean => {
setIsAuthenticated(boolean);
};
return (
<Fragment>
<Router>
<div className="container">
<Switch>
<Route
exact
path="/login"
render={props =>
!isAuthenticated ? (
<Login {...props} setAuth={setAuth} />
) : (
<Redirect to="/dashboard" />
)
}
/>
<Route
exact
path="/register"
render={props =>
!isAuthenticated ? (
<Register {...props} setAuth={setAuth} />
) : (
<Redirect to="/dashboard" />
)
}
/>
<Route
exact
path="/dashboard"
render={props =>
isAuthenticated ? (
<Dashboard {...props} setAuth={setAuth} />
) : (
<Redirect to="/login" />
)
}
/>
</Switch>
</div>
</Router>
</Fragment>
);
}
export default App;
and this is the code for my back-end dashboard routes, where I think the miscommunication may be happening? Any help would be greatly appreciated.
const router = require("express").Router();
const db = require("../db");
const authorization = require('../middleware/authorization');
router.get("/", authorization, async(req, res) => {
try {
// req.user comes from authorization, contains jwt payload
const user = await db.query("SELECT user_name, user_email FROM users WHERE user_id = $1", [req.user]);
res.json(user.rows[0]);
} catch (err) {
console.err(err.message)
res.status(500).json("Server Error");
}
});
module.exports = router;
Sorry about the confusion! I realized I didn't leave enough information, but I also traced down the source of the issue almost immediately after posting. It was a simple issue with the token variable being mislabeled.
I have MERN application which uses http only cookies with jwt as an authentication. I made a provider in React to hold the user information. There are also protected routes on the front-end. On the backend I have a route which returns the current user if a cookie is present else I set the user to null. When the app starts, refreshes or the user changes I make this call. Everything works as expected.
The problem appears when I refresh the page or use the url bar to manually navigate to a certain page. Because in both cases the app refreshes there is a short amount of time where a user is not present until the fetch finishes. As a result I am always redirected to the homepage instead of the page I refreshed on or typed. I tried setting a 'Loading' state but it only works for the App Component.
Example: I am on '/details/1234' I click refresh and I am sent back to '/'.
Is there any way to fix this behaviour? Maybe some changes in the router or forcing React to not do anything until that fetch is finished? Thanks in advance.
Here is the code for my provider:
import { createContext, useEffect, useState } from 'react';
export const AuthContext = createContext();
function AuthContextProvider(props) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
async function initialUser() {
try {
const response = await fetch(`/api/users/profile`, {
headers: {
"Content-Type": "application/json"
},
method: "GET",
credentials: 'include'
});
setLoading(false);
const userData = await response.json();
return userData.message ? setUser(null) : setUser(userData)
} catch (error) {
console.log(error);
}
}
if (!user) {
initialUser();
}
}, [user]);
return (
<AuthContext.Provider value={{ user, loading, setUser }}>
{props.children}
</AuthContext.Provider>
)
}
export default AuthContextProvider
Here is the code for the App Component. The App Component is wrapped by the Provider and the Router in index.js
import { useContext } from 'react';
import { Switch, Route, Redirect } from 'react-router-dom';
import './App.css';
import { AuthContext } from './context/AuthContext';
import Navigation from './components/Navigation/Navigation';
import Footer from './components/Footer/Footer';
import Homescreen from './components/HomeScreen/HomeScreen';
import LandingScreen from './components/LandingScreen/LandingScreen';
import SignScreen from './components/SignScreen/SignScreen';
import Search from './components/Search/Search';
import Genres from './components/Genres/Genres';
import Details from './components/Details/Details';
import Watch from './components/Watch/Watch';
import Profile from './components/Profile/Profile';
function App() {
const { user, loading } = useContext(AuthContext);
return (
!loading
? <div className="App">
<Navigation user={user} />
<Switch>
<Route exact path="/" render={() => (!user ? <LandingScreen /> : <Homescreen />)} />
<Route path="/sign-in" render={() => (!user ? <SignScreen /> : <Redirect to="/" />)} />
<Route path="/sign-up" render={() => (!user ? <SignScreen /> : <Redirect to="/" />)} />
<Route path="/profile" render={() => (!user ? <Redirect to="/sign-in" /> : <Profile />)} />
<Route path="/search" render={() => (!user ? <Redirect to="/sign-in" /> : <Search />)} />
<Route path="/movies/:genre" render={() => (!user ? <Redirect to="/sign-in" /> : <Genres />)} />
<Route path="/details/:id" render={() => (!user ? <Redirect to="/sign-in" /> : <Details />)} />
<Route path="/watch/:id" render={() => (!user ? <Redirect to="/sign-in" /> : <Watch />)} />
</Switch>
<Footer />
</div>
: null
);
}
export default App;
You are updating the loading state to false before you update the user state. You should update the user data first then update the loading state. When you update the loading state before the asynchronous JSON response handling there will be at least 1 render cycle between them, allowing the router to render the routes with the still undetermined user state. I suggest using the finally block of the try/catch so that no matter what, when the fetch processing completes the loading state is updated.
function AuthContextProvider(props) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
async function initialUser() {
try {
const response = await fetch(`/api/users/profile`, {
headers: {
"Content-Type": "application/json"
},
method: "GET",
credentials: 'include'
});
const userData = await response.json();
userData.message ? setUser(null) : setUser(userData)
} catch (error) {
console.log(error);
} finally {
setLoading(false);
}
}
if (!user) {
initialUser();
}
}, [user]);
return (
<AuthContext.Provider value={{ user, loading, setUser }}>
{props.children}
</AuthContext.Provider>
)
}
app.js
const [isLoggedIn, setIsLoggedIn] = useState(false);
useEffect(() => {
fetch('http://localhost:5000/usersystem/user/isloggedin', {
method: 'GET',
credentials: 'include',
mode: 'cors'
}).then(response => response.json())
.then(response => {
setIsLoggedIn(response.success);
})
}, []);
return (
<div className="App">
<AppContext.Provider value={{ isLoggedIn, setIsLoggedIn }}>
<Routes />
</AppContext.Provider>
</div>
);
}
routes.js
export default function Routes() {
return (
<Switch>
<ProtectedRoute exact path="/">
<Dashboard />
</ProtectedRoute>
<Route exact path="/login">
<Login />
</Route>
<Route>
<Notfound />
</Route>
</Switch>
);
}
protectedroute.jsx
import React from "react";
import { Route, Redirect, useLocation } from "react-router-dom";
import { useAppContext } from "../libs/ContextLib";
export default function ProtectedRoute({ children, ...rest }) {
const { pathname, search } = useLocation();
const { isLoggedIn } = useAppContext();
return (
<Route {...rest}>
{isLoggedIn ? (
children
) : (
<Redirect to={
`/login?redirect=${pathname}${search}`
} />
)}
</Route>
);
}
I have a node express app on port 5000, which handles user authentication with passport and cookie session. It works all good, the session cookies gets to the browser, and can read it and can send it back to authenticate the user.
However, in react, when i refresh the browser the user gets redirected to the login page instead of the dashboard, even with the session cookies in the browser.
It feels like the setIsLoggedIn(response.success) in app.js, wont update isLoggedIn before it gets to the ProtectedRoute and redirects to login.
Im into this for 4 hours now, and couldnt solve it. Can someone please suggest me a way to do this, or tell me what is the problem, so when i refresh the page it wont redirect to login? Thanks.
Your component is rendered once when you create the component.
Your setIsLoggedIn will update the state and re-render it.
You could put in a Loader to wait for the data to come before displaying the page you want.
E.g:
function MyComponent () {
const [isLoggedIn, setIsLoggedIn] = useState(null);
useEffect(() => {
fetch('http://localhost:5000/usersystem/user/isloggedin', {
method: 'GET',
credentials: 'include',
mode: 'cors'
}).then(response => response.json())
.then((response, err) => {
if(err) setIsLoggedIn(false); // if login failure
setIsLoggedIn(response.success);
})
}, []);
if(isLoggedIn === null) return (<div>Loading</div>);
// this is only rendered once isLoggedIn is not null
return (
<div className="App">
<AppContext.Provider value={{ isLoggedIn, setIsLoggedIn }}>
<Routes />
</AppContext.Provider>
</div>
);
}
I am making my first react app with user authentication on my Rails Api (DoorKeeper/Devise). I set the expires_at in react's local_storage and try to verify in the private route component. I want to verify a user token to give him access to routes in this case '/'. I cant reach userSignedIn. how do I reach this function from the PrivateRoute component?
// PrivateRoute.js
import React from 'react';
import { Route, Redirect } from 'react-router-dom'
import userSignedIn from '../auth'
const PrivateRoute = ({ component: Component, ...rest }) => (
<Route {...rest} render={(props) => (
userSignedIn === true
? <Component {...props} />
: <Redirect to={{
pathname: '/login',
state: { from: props.location }
}} />
)} />
)
export default PrivateRoute;
// auth.js
const userSignedIn = () => {
if (localStorage.getItem('expires_at') !== null) {
return new Date() < new Date(localStorage.getItem('expires_at'))
} else {
return false
}
}
export default userSignedIn;
Thanks
Haven't had time to test this but
const PrivateRoute = ({ component: Component, ...rest }) => (
<Route {...rest} render={(props) => (
userSignedIn() === true
? <Component {...props} />
: <Redirect to={{
pathname: '/login',
state: { from: props.location }
}} />
)} />
)
You need to call the function userSignedIn() since it's what returns the value true/false (that's the only change here) userSignedIn === true -> userSignedIn() === true
I am writing a React.js application (v15.3) using react-router (v2.8.1) and ES6 syntax. I cannot get the router code to intercept all transitions between pages to check if the user needs to login first.
My top level render method is very simple (the app is trivial as well):
render()
{
return (
<Router history={hashHistory}>
<Route path="/" component={AppMain}>
<Route path="login" component={Login}/>
<Route path="logout" component={Logout}/>
<Route path="subject" component={SubjectPanel}/>
<Route path="all" component={NotesPanel}/>
</Route>
</Router>
);
}
All the samples on the web use ES5 code or older versions of react-router (older than version 2), and my various attempts with mixins (deprecated) and willTransitionTo (never gets called) have failed.
How can I set up a global 'interceptor function' to force users to authenticate before landing on the page they request?
Every route has an onEnter hook which is called before the route transition happens. Handle the onEnter hook with a custom requireAuth function.
<Route path="/search" component={Search} onEnter={requireAuth} />
A sample requireAuth is shown below. If the user is authenticated, transition via next(). Else replace the pathname with /login and transition via next(). The login is also passed the current pathname so that after login completes, the user is redirected to the path originally requested for.
function requireAuth(nextState, replace, next) {
if (!authenticated) {
replace({
pathname: "/login",
state: {nextPathname: nextState.location.pathname}
});
}
next();
}
In v4 you just create a route component that checks if uses is authenticated and return the next components and of course next component can be other routes.
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { Route, Redirect } from 'react-router-dom';
import AuthMiddleware from 'modules/middlewares/AuthMiddleware';
class PrivateRoute extends Component {
static propTypes = {
component: PropTypes.func.isRequired,
isAuthenticated: PropTypes.bool,
isLoggedIn: PropTypes.func.isRequired,
isError: PropTypes.bool.isRequired
};
static defaultProps = {
isAuthenticated: false
};
constructor(props) {
super(props);
if (!props.isAuthenticated) {
setTimeout(() => {
props.isLoggedIn();
}, 5);
}
}
componentWillMount() {
if (this.props.isAuthenticated) {
console.log('authenticated');
} else {
console.log('not authenticated');
}
}
componentWillUnmount() {}
render() {
const { isAuthenticated, component, isError, ...rest } = this.props;
if (isAuthenticated !== null) {
return (
<Route
{...rest}
render={props => (
isAuthenticated ? (
React.createElement(component, props)
) : (
<Redirect
to={{
pathname: isError ? '/login' : '/welcome',
state: { from: props.location }
}}
/>
)
)}
/>
);
} return null;
}
}
const mapStateToProps = (state) => {
return {
isAuthenticated: state.auth.isAuthenticated,
isError: state.auth.isError
};
};
const mapDispatchToProps = (dispatch) => {
return bindActionCreators({
isLoggedIn: () => AuthMiddleware.isLoggedIn()
}, dispatch);
};
export default connect(mapStateToProps, mapDispatchToProps)(PrivateRoute);
This version of the onEnter callback finally worked for react-router (v2.8):
requireAuth(nextState,
replace)
{
if(!this.authenticated()) // pseudocode - SYNCHRONOUS function (cannot be async without extra callback parameter to this function)
replace('/login')
}
The link which explains react-router redirection differences between V1 vs v2 is here. Relevant section quoted below:
Likewise, redirecting from an onEnter hook now also uses a location descriptor.
// v1.0.x
(nextState, replaceState) => replaceState(null, '/foo')
(nextState, replaceState) => replaceState(null, '/foo', { the: 'query' })
// v2.0.0
(nextState, replace) => replace('/foo')
(nextState, replace) => replace({ pathname: '/foo', query: { the: 'query' } })
Full Code Listing Below (react-router version 2.8.1):
requireAuth(nextState,
replace)
{
if(!this.authenticated()) // pseudocode - SYNCHRONOUS function (cannot be async without extra callback parameter to this function)
replace('/login');
}
render() {
return (
<Router history={hashHistory}>
<Route path="/" component={AppMain}>
<Route path="login" component={Login}/>
<Route path="logout" component={Logout}/>
<Route path="subject" component={SubjectPanel} onEnter={this.requireAuth}/>
<Route path="all" component={NotesPanel} onEnter={this.requireAuth}/>
</Route>
</Router>
);
}
If you're using react-router 4 and above, use Render props and redirect to solve this. Refer: onEnter not called in React-Router
This is not a safe solution
You can try n use useEffect hook on every page requiring login as:
useEffect(() => {
const token = localStorage.getItem('token');
if(!token) {
history.push('/login');
}
}
This uses useHistory hook from 'react-router-dom'
you just need to initialize it before calling it as:
const history = useHistory();
As already stated above it is not a safe sloution, but a simple one
You can do it by simply creating a RestrictedRoute or Private component and pass your redux user authenticated state to this and in RestrictedRoute component redirect if state is false, in case of true redirect to component.
code:
const RestrictedRoute = ({ component: Component, authUser, auth, ...rest }) => (
<Route
{...rest}
render={props =>
auth.is_authenticated && auth.is_authorized && authUser ? (
<Component {...props} />
) : (
<Redirect
to={{
pathname: '/signin',
state: { from: props.location },
}}
/>
)
}
/>
);
<Switch>
<RestrictedRoute path={`${match.url}app`}
authUser={authUser}
auth={{ is_authenticated, is_authorized }}
component={MainApp}
/>
<Route path='/signin' component={SignIn} />
<Switch>