I'm making the website with React and React Router with Redux. Lots of routes(pages) requires logged in. I can redirect to login if user is not logged in like this:
function requireAuth(nextState, replace) {
let loggedIn = store.getState().AppReducer.UserReducer.loggedIn;
if(!loggedIn) {
replace({
pathname: '/login',
state: {
nextpathname: nextState.location.pathname
}
});
}
}
ReactDOM.render(
<Provider store={store}>
<Router history={history}>
<Route path="/" component={App}>
<IndexRoute component={Index} />
<Route path="login" component={Login} />
<Route path="register" component={Register} />
<Route path="dashboard" component={Graph} onEnter={requireAuth}>
... some other route requires logged in ...
</Route>
</Route>
</Router>
</Provider>,
document.getElementById('entry')
);
Please see the code, I used onEnter hook to redirect to '/login' route if user is not logged in. Data for checking user logged in is in the store, and it will update after user logged in.
It's working perfectly, but problem is when I refresh the page, store will be reset and user is not logged in state back.
I know this happens because Redux store is just memory storage, so refesh page will loose all data.
Check the server session on every refresh can be work but this might be too much request, so that looks bad idea.
Save the logged in state data into localStorage might be work, but in this case, I should check every AJAX calls fail that request rejected because session is expired or not exists like something, and it looks like bad idea either.
Is there a way to solve this problem more clearly? My website will gonna use lots of people, so I want to reduce XHR calls as possible.
Any advice will be very appreciated.
Another way to go is to use JSON Web Tokens (JWT) that are required for each route, and localStorage to check for the JWT.
TL;DR
On the front end you have a signin and signup route that queries your
server for a JWT according to the authentication on the server. Once
passed the appropriate JWT you would then set a property of state to
true. You can have a signout route that allows the user to set this
state to false.
The index.js which contains your routes can check local storage
before rendering, thus eliminating your problem with losing the state
on refresh but keeping some security.
All routes requiring authentication in your application are rendered
through a Composed Component, and secured with the necessity of
having JWTs in the header for authorization on the server API.
Setting this up takes a little time but it will make your application 'reasonably' secure.
To solve your problem:
Check the local storage before the routes in your index.js
file as shown below, updating the state to authenticated if required.
The application maintains security with the fact that the API is secured by the JWT which would solve your refresh issue, and maintain a secure link to your server and data.
Thus in the routes you would have something like this:
index.js
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import { createStore, applyMiddleware, compose } from 'redux';
import { Router, Route, browserHistory, IndexRoute } from 'react-router';
import reduxThunk from 'redux-thunk';
import { AUTHENTICATE_THE_USER } from './actions/types';
import RequireAuth from './components/auth/require_auth';
import reducers from './reducers';
/* ...import necessary components */
const createStoreWithMiddleware = compose(applyMiddleware(reduxThunk))(createStore);
const store = createStoreWithMiddleware(reducers);
/* ... */
// Check for token and update application state if required
const token = localStorage.getItem('token');
if (token) {
store.dispatch({ type: AUTHENTICATE_THE_USER });
}
/* ... */
ReactDOM.render(
<Provider store={store}>
<Router history={history}>
<Route path="/" component={App}>
<IndexRoute component={Index} />
<Route path="login" component={Login} />
<Route path="register" component={Register} />
<Route path="dashboard" component={RequireAuth{Graph}} />
<Route path="isauthenticated" component={RequireAuth(IsAuthenticated)} />
... some other route requires logged in ...
</Route>
</Router>
</Provider>
, .getElementById('entry'));
RequiredAuth
is the composed component while Graph
and IsAuthenticated
(can be any number of appropriately named components) require the state.authenticated
to be true.
The Components, in this case Graph
and IsAuthenticated
rendered if the state.authenticated
is true. Otherwise is defaults back to the root route.
Then you could build a Composed Component like this, through which all your routes are rendered. It will check that the state in which you are holding whether or not the user is authenticated (a boolean) is true before rendering.
require_auth.js
import React, { Component } from 'react';
import { connect } from 'react-redux';
export default function (ComposedComponent) {
// If user not authenticated render out to root
class Authentication extends Component {
static contextTypes = {
router: React.PropTypes.object
};
componentWillMount() {
if (!this.props.authenticated) {
this.context.router.push('/');
}
}
componentWillUpdate(nextProps) {
if (!nextProps.authenticated) {
this.context.router.push('/');
}
}
render() {
return <ComposedComponent {...this.props} />;
}
}
function mapStateToProps(state) {
return { authenticated: state.authenticated };
}
return connect(mapStateToProps)(Authentication);
}
On the signup/signin side you could create an action that stores the JWT and sets up the state to authenticated through an action-creator -> redux store. This example makes use of axios to run the async HTTP request response cycle.
export function signinUser({ email, password }) {
// Note using the npm package 'redux-thunk'
// giving direct access to the dispatch method
return function (dispatch) {
// Submit email and password to server
axios.post(`${API_URL}/signin`, { email, password })
.then(response => {
// If request is good update state - user is authenticated
dispatch({ type: AUTHENTICATE_THE_USER });
// - Save the JWT in localStorage
localStorage.setItem('token', response.data.token);
// - redirect to the route '/isauthenticated'
browserHistory.push('/isauthenticated');
})
.catch(() => {
// If request is bad show an error to the user
dispatch(authenticationError('Incorrect email or password!'));
});
};
}
You would also need to set up your store (Redux in this case) and action creator of course.
The 'real' security comes from the back end. And to do this you use localStorage to keep the JWT on the front end and pass it in the header to any API calls that have sensitive/protected information.
Creating and parsing the JWT for users on the server API is another step. I have found passport to be effective.
Why not using sessionStorage with the logged in state and expiration date? You will have to write more code for checking the sessionStorage state but that's the only way in my opinion you can save the XHR call from being sent.