I want to protect routes and make them available only for authenticated users.
the problem is that I know if the user is authenticated or not after doing a fetch() and updating the state in the context (and that takes some time)
I want to redirect users to /login when the fetch is complete and the context state isAuthenticated = false
import { Route, Switch } from 'react-router-dom'
import { MyContext } from './Context'
...
<MyContext.Consumer>
{(context) => (
<Switch>
<Route path="/" exact strict component={Homepage}/>
<Route path="/this-is-a-protected-route" exact strict component={Admin}/>
<Route path="/login" exact strict component={Login}/>
</Switch>
)}
</MyContext.Consumer>
this is the context
export const MyContext = React.createContext()
export class MyProvider extends Component {
state = {
'isAuthenticated': false,
}
componentDidMount() {
fetch('/authenticated')
.then(
function(response) {
response.json().then(function(data) {
this.setState({'isAuthenticated': true})
});
}
)
.catch(function(error) {
console.log('fetch error', error);
});
}
render() {
return (
<MyContext.Provider value={{
isAuthenticated: this.state.isAuthenticated
}}>
{this.props.children}
</MyContext.Provider>
)
}
}
Ideally I will be redirected to /login only after hitting /this-is-a-protected-route and the state.isAuthenticated = false (but this is the default value!)
I removed some code for brevity so we can focus on the problem. Hope you understand thanks!
You can render protected routes only if user authenticated
{context.isAuthenticated ?
<Route path="/this-is-a-protected-route" exact strict component={Admin}/> :
<Redirect to='/login' />
}
Private routes can be moved to separate component.
More details and examples here.
Related
I am using react-router-dom to handle routes. I am making a simple login page where you login, session gets generated and redirects to home page.
I want to have a check on my home page to say if the user doesn't have a valid session, throw them back to the login page ("/"). I have been at this for hours and my problem is, axios is async so I haven't found a way to not load the page till this check is done, and the solution I have made shows the page for a split second while this call to my API is being made.
What is the standard way of doing this? My method is below:
App.js
import React, { Component } from 'react'
import Login from './Login';
import Home from './Home';
import { Route, Redirect } from 'react-router-dom';
import axios from 'axios'
export class App extends Component {
constructor(props) {
super(props)
this.state = {
homeRoute: <Route path="/home" component={Home}></Route>
}
}
getAuthorisedAPI() {
axios.get("http://localhost:8080/validateSession")
.then(response => {
console.log(response.data.Success)
if(!response.data.Success) {
this.setState({
homeRoute: <Route path="/home" component={Home}><Redirect to="/" /></Route>
})
}
}).catch(error => {
console.log(error)
})
}
componentDidMount() {
this.getAuthorisedAPI()
}
render() {
axios.defaults.withCredentials = true
return (
<>
<Route exact path="/" component={Login} />
{
this.state.homeRoute
}
</>
)
}
}
export default App
If there was a way axios could do a non async call so the page load waits for this response then the below would be ideal..
const isValid = true
return (
<>
<Route exact path="/" component={Login} />
<Route path="/home" component={Home}>
{
!isValid && <Redirect to="/" />
}
</Route>
</>
)
Thanks
Since you are on client side, I'd recommend to split your app in two. The <AuthenticatedApp /> and the <UnauthenticatedApp />. You can read more about this on https://kentcdodds.com/blog/authentication-in-react-applications.
The other option is to use async/await.
async getAuthorisedAPI() {
try {
const response = await axios.get("http://localhost:8080/validateSession");
console.log(response.data.Success)
if(!response.data.Success) {
this.setState({
homeRoute: <Route path="/home" component={Home}><Redirect to="/" /></Route>
})
}
} catch(error) {
console.log(error)
}
}
async componentDidMount() {
await this.getAuthorisedAPI()
}
Currently I have the following react router
<BrowserRouter>
<Switch>
{/* It should display the register page if the isAuthenticated state is true */}
<Route exact path="/register" render={(props) => ((this.state.isAuthenticated) ? <RegisterPage /> : <NewLandingPage {...props} /> )} />
<Route exact path="/login" render={(props) => <LoginPage />} />
</Switch>
</BrowserRouter>
In my App.js
And then in the constructor of my App.js I have the following states, and authservice call
this.state = {
isAuthenticated: false
}
isAuthenticated().then((result) => {
if (result == true) {
this.state.isAuthenticated = true;
console.log("its true authenticated");
console.log(this.state.isAuthenticated);
console.log("----")
} else {
this.state.isAuthenticated = false;
console.log("its false not autheticated");
console.log(this.state.isAuthenticated);
console.log("----")
}
});
By default, my isAuthenticated state is false. So i wouldnt be able to display "/register" if I hadnt an auth service.
But I have implemented an isAuthenticated() to make the app.js check if theres a valid user. It actually displays the desired data. For example, if im autheticated, the result would be true, and I would receive the following in my console.
console.log("its true authenticated");
console.log(this.state.isAuthenticated); //true
But seems that the browserrouter is not taking into effect the isAuthenticated() method, and is relying solely in the default value, which is false, so It will always hide my register page will never be displayed besides me having an authservice there
this.state = {
isAuthenticated: false
}
Any idea of why is my browser not detecting the states change?
EDIT:
isAuthenticated() function by request
import axios from 'axios'
import Cookies from 'js-cookie';
export default function isAuthenticated(){
var accesstoken = Cookies.get('accesstoken');
return axios({ method: 'post', url: 'http://localhost:3003/verify', headers: { Authorization: `Bearer ${accesstoken}` } })
.then(function (response) {
if(response.data.status === "valid"){
console.log("service valid")
return true;
}else{
console.log("service invalid")
return false;
}
});
}
When you change the state with redux(for example with an action you bring the component from actions in redux, and it sets new state in the reducer), it will effect after the component mount, there is a way to render the component(If the props have changed) and get the new state status, with ComponentDidUpdate method:
So the actions effect the global state(in reducer), and you bring this state to your component. ComponentDidUpdate will take a look and when the state you bring with the props has changed, it will update the component and allow you to execute setState
ComponentDidUpdate(prevProps) {
// Typical usage (don't forget to compare props):
if (this.props.isAuthenticated !== prevProps.isAuthenticated) {
this.setState({
isAuthenticated:true
});
// I suppose you call the state from the reducer by this.props.isAuthenticated
}
}
Also for checking the authentication state, I am using an auth.js util like this, it gets the authenticated state from reducer, makes a redirect to login page instead of the component, if authenticated is false:
import React from "react";
import { Route, Redirect } from "react-router-dom";
import { connect } from "react-redux";
import PropTypes from "prop-types";
const AuthRoute = ({ component: Component, authenticated, ...rest }) => (
<Route
{...rest}
render={props =>
authenticated === true ? (
<Redirect to="/user" />
) : (
<Component {...props} />
)
}
/>
);
const mapStateToProps = state => ({
authenticated: state.user.authenticated
});
AuthRoute.propTypes = {
user: PropTypes.object
};
export default connect(mapStateToProps)(AuthRoute);
so in my app.js, I can check the auth state and redirect if necessary:
import AuthRoute from "./util/AuthRoute.js";
<Switch>
<Route exact path="/user" component={home} />
<AuthRoute exact path="/login" component={userLogin} />
<AuthRoute exact path="/signup" component={userSignup} />
<Route exact path="/ticketList" component={ticketList} />
</Switch>
Possibly the issue is that the authentication code is in your constructor which gets called once. You might want to try it a slightly different way. Create a const (outside your class) to hold the switchable component content:
export const RegisterOrLandingPage = () => {
if (isAuthenticated()) {
return <RegisterPage />;
}
return <NewLandingPage />;
}
Your isAuthenticated() code should ideally be in a function too, rather than sat in the constructor or used as a state, and abstracted out to its own component (i.e., callable from anywhere, usable by anyone).
Then your route can be slightly different:
<Route exact path="/register" component={RegisterOrLandingPage} />
I have a React front-end and Python back-end with user session management using flask-login's HttpOnly Session Cookies. How can I restrict react-router-dom routes based on this type of session management? I've created a ProtectedRoute component:
import { Route, Redirect } from 'react-router-dom';
class ProtectedRoute extends Component {
constructor(props) {
super(props);
this.state = {
authenticated: false,
}
}
render() {
const { component: Component, ...props } = this.props
return (
<Route
{...props}
render={props => (
this.state.authenticated ?
<Component {...props} /> :
<Redirect to='/login' />
)}
/>
)
}
}
export default ProtectedRoute;
Is it possible to set this.setState({authenticated: true}) based on the existing session?
Why not pass authenticated (or isEnabledin my example) as a prop? ProtectedRoute will rerender when its props change. This is what I use in my React applications:
import React from 'react';
import { Route, Redirect } from 'react-router-dom';
const ProtectedRoute = ({isEnabled, ...props}) => {
return (isEnabled) ? <Route {...props} /> : <Redirect to="/login"/>;
};
export default ProtectedRoute;
And then you can just use this like so:
<ProtectedRoute path="/dashboard" isEnabled={isAuthenticated()} />
I know this question is old but I was looking for the same thing. Here's my routes.js file:
import auth from './services/auth'
const PrivateRoute = ({isAuthenticated, ...props}) => {
return (isAuthenticated) ? <Route {...props} /> : <Redirect to="/login"/>;
};
class Routes extends React.Component {
constructor(){
super();
this.state = {
isAuthenticated: false
}
}
componentDidMount(){
auth.get('')
.then( async (response) => {
const status = await response.status
if (status === 200) {
this.setState({isAuthenticated: true})
} else {
this.setState({isAuthenticated: false})
}
})
.catch( async (error) => console.log(error))
}
render() {
return (
<BrowserRouter>
<Switch>
<Route path="/login" exact component={Login} />
<PrivateRoute isAuthenticated={this.state.isAuthenticated} path="/" component={() => "barra"}/>
<PrivateRoute isAuthenticated={this.state.isAuthenticated} path="/home" component={() => "home"}/>
<PrivateRoute isAuthenticated={this.state.isAuthenticated} path="/profile" component={() => "profile"}/>
</Switch>
</BrowserRouter>
)
};
}
And auth import is:
const axios = require('axios');
axios.defaults.withCredentials = true;
const auth = axios.create({
baseURL: "http://localhost:5000/auth"
})
export default auth;
So, basically I have a Flask app with Flask-Login running on another local server (with CORS enabled THIS IS VERY IMPORTANT) and if 200 is returned, user on react is authenticated.
I am trying to implement some server side authentication (via xhr) while using React Router v4. I do not want the route to transition until I validate with my server that a user is authenticated (by having a token) as well as the token is stored in session storage (not that this needs to be async).
Currently the issue is that my "private" route is still trying to render even though the user is not authenticated.
My React Router routes look like:
class AppContainer extends Component {
render() {
return (
<div>
<main>
<Switch>
<Route exact path='/' component={Home} />
<PrivateRoute path='/dashboard' component={Dashboard} />
</Switch>
</main>
</div>
);
}
}
PrivateRoute, as specified looks like:
const isAuthenticated = async () => {
const resp = await axios.get('http://localhost/api/session');
const token = _.get(resp, 'data.success');
const authObj = storage.getFromSession('TOKEN');
return !_.isNil(_.get(authObj, 'Token')) && token;
};
const PrivateRoute = ({ component: Component, ...rest }) => (
<Route {...rest} render={props => (
isAuthenticated() ? (
<Component {...props}/>
) : (
<Redirect to={{
pathname: '/',
state: { from: props.location }
}}/>
)
)}/>
)
export default PrivateRoute;
The Dashboard is trying to render even though the user is not authenticated. How would I wait for my api call to be returned and then redirect the user to either /dashboard or / (home page)?
My last try you can use a component like this:
import React, {PropTypes} from 'react';
import {Redirect} from 'react-router-dom';
export default class PrivateRoute extends React.Component {
constructor(props) {
super(props);
this.state={loading:true,authenticated:false}
}
componentDidMount(){
/* your authentication logic...*/
setTimeout(()=>{
this.setState({loading:false,authenticated:true});
},3000)
}
render() {
if(this.state.loading)
return <h1>Loading</h1>;
if(this.state.authenticated)
return (this.props.children);
else
return <Redirect to="/" />
}
}
And use it in your router like this:
<Route path="/your-protected-route" component={()=><PrivateRoute><YourComponent /></PrivateRoute>} />
I am trying to authenticate private routes. I have it working by checking for a cookie before allowing access. However, cookies can be spoofed so I have an API end point which accepts the cookie and returns whether its valid or not.
Working version without API:
const PrivateRoute = ({ component: Component, ...rest }) => (
<Route {...rest} render={props => (
cookies.get('sessionid') ? (
<Component {...props}/>
) : (
<Redirect to={{
pathname: '/',
state: { from: props.location }
}}/>
)
)}/>
)
ReactDOM.render((
<Router>
<Switch>
<Route exact path="/" component={Login}/>
<PrivateRoute exact path="/home" component={Home} />
<PrivateRoute exact path="/upload" component={Upload}/>
<PrivateRoute exact path="/logout" component={Logout}/>
<PrivateRoute exact path="/review" component={Review}/>
<Route component={ NotFound } />
</Switch>
</Router>
), document.getElementById('root'))
Additional code for API call:
axios.post(process.env.REACT_APP_API_URL+'/account/validate-session', {
t1_session_id: cookies.get('sessionid')
})
.then(function (response) {
if(response.data.status === "OK"){
console.log('authenticated go to private route');
} else {
console.log('not valid, redirect to index');
}
}.bind(this))
.catch(function (error) {
console.log('not valid, redirect to index');
}.bind(this));
The issue is I am not sure how to incorporate the API section of code into the main route section.
Thanks
Well, I guess you must write a wrapper component for it. Let's try:
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import axios from 'axios';
import YourComponent from './path/to/YourComponent';
class WrapperComponent extends Component {
constructor(props) {
super(props);
this.state = {
isAuth: false
};
}
componentDidMount() {
axios.post(process.env.REACT_APP_API_URL+'/account/validate-session', {
t1_session_id: cookies.get('sessionid')
})
.then(function (response) {
if(response.data.status === "OK"){
this.setState({isAuth: true})
} else {
this.setState({isAuth: false})
}
}.bind(this))
.catch(function (error) {
console.log('not valid, redirect to index');
}.bind(this));
}
render() {
return (this.state.isAuth ? <YourComponent /> : null);
}
}
export default WrapperComponent;
And now your route must redirect to WrapperComponent:
const PrivateRoute = ({ component: WrapperComponent, ...rest }) => (
<Route {...rest} render={props => (
<WrapperComponent {...props}/>
)}/>
)
Every user can only spoof his own cookie. Likewise, a user can spoof the entire JS code that runs in his browser. Therefore, calling the server for verifying the cookie will not mitigate your vulnerability so much.
Instead, you should trust the cookie in your JS code, and check the cookie in every operation in the server that requires login.
Notice that a cookie is not the safest way of identifying a user since it's a subject to CSRF attack. You should either use JWT or check the referer.
Anyway if you decide to adhere your idea of asking the server whether the cookie is valid, I would adopt arikanmstf's answer, except you should handle three cases: auth=true, auth=false and auth=null, since if we're yet to get an answer from the server we want just to hide the element, but if we got a negative answer, we wanna redirect the user.
constructor(props) {
super(props);
this.state = {
isAuth: null
};
}
render() {
if(this.state.isAuth === null) return null;
return (this.state.isAuth ? <YourComponent /> : <Redirect ... />
}