Login App with CSRF protection - Implement authentication in ReactJS using secure REST API - Part 3
Today we’ll show you how to create a login application with XSS and CSRF protection. As you know we have divided the whole series in three parts and it’s the last part of the article.
Login App with CSRF protection - Implement authentication in ReactJS using secure REST API, Build a React.js Application with User Login and Authentication, login form in react js using localStorage, cookie and redux store, Authentication For Your React and Express Application with JWT access token and refresh token, Protected routes and Authentication with React and Node.js, Authentication using JWT from ReactJS Single Page Application, Prevent Cross-site scripting (XSS), Cross-site request forgery (CSRF/XSRF) attack.
If you have seen the previous articles of the ReactJS where we talked about the authentication in ReactJS using Node.js API. So additionally, we will let you know the secure way to implement authentication in ReactJS in today’s article.
Checkout more articles on ReactJS
- Socket.IO – How to implement Socket.IO in ReactJS
- How to reset the state of a Redux store
- Implement dropdown in ReactJS
- Set environment variables in ReactJS
- Search filter for multiple object in ReactJS
We planned to divide this article into three parts.
-
Part 1 – Understanding authentication using JWT access token and refresh token with CSRF protection
-
Part 2 – Implement authentication in Node.js using JWT token with CSRF protection
-
Part 3 – Implement authentication in ReactJS using REST API with CSRF protection (You are here…)
Way to create login application in ReactJS using secure REST API
- Create secure REST API in Node.js
- Setup react app
- Create react components
- Implement react router
- Add services to call API
- Implement redux
- Create route guard
- Connect components to the redux store
- Output
1. Create secure REST API in Node.js
To create a secure login application, first we have to create a REST API so we can consume it into the react application. We have already created the REST API in Node.js for authentication. You can create REST API in any backend technologies.
If you don’t want to create an API then download the source code and run the project.
2. Setup react app
Let’s setup the basic react application using `create-react-app` to implement the authentication in ReactJS. In this article, mostly we’ll use the React Hooks.
Check out the following file structure of the react application that you should prefer.
Login App - File structure - Clue Mediator
3. Create react components
In this article, we’ll create components like Login & Dashboard and some styles in the css file. You can create more than it but for the demo purposes we have considered only two components.
- Login component - It will contain a simple login form where we can call the API to validate the user. On successful authentication, we will redirect you to the Dashboard component.
- Dashboard component - It’s accessible only for the authenticated user. In this page, we will call one more API to get the user list with the help of the access token. We will have one more button to manage the logout.
App.js
import React from 'react';
function App() {
return (
<div className="App">
<div className="header">
<a href="/login">Login</a>
<a href="/dashboard">Dashboard</a>
</div>
<div className="content">
Login Application
</div>
</div>
);
}
export default App;
pages/Login.js
import React, { useState } from 'react';
function Login() {
const username = useFormInput('');
const password = useFormInput('');
// handle button click of login form
const handleLogin = () => {
}
return (
<div>
Login<br /><br />
<div>
Username<br />
<input type="text" {...username} autoComplete="new-password" />
</div>
<div style={{ marginTop: 10 }}>
Password<br />
<input type="password" {...password} autoComplete="new-password" />
</div>
<input
type="button"
style={{ marginTop: 10 }}
value="Login"
onClick={handleLogin} />
</div>
);
}
// custom hook to manage the form input
const useFormInput = initialValue => {
const [value, setValue] = useState(initialValue);
const handleChange = e => {
setValue(e.target.value);
}
return {
value,
onChange: handleChange
}
}
export default Login;
pages/Dashboard.js
import React, { useState } from 'react';
function Dashboard() {
const [userList, setUserList] = useState([]);
// handle click event of the logout button
const handleLogout = () => {
}
return (
<div>
Welcome!<br /><br />
<input type="button" onClick={handleLogout} value="Logout" /><br /><br />
<input type="button" value="Get Data" /><br /><br />
<b>User List:</b>
<pre>{JSON.stringify(userList, null, 2)}</pre>
</div>
);
}
export default Dashboard;
index.css
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
"Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
monospace;
}
.content {
padding: 20px;
}
.header {
padding: 10px;
background: #edf2f4;
border-bottom: 1px solid #999;
}
.header a {
color: #0072ff;
text-decoration: none;
margin-left: 20px;
margin-right: 5px;
}
.header a:hover {
color: #8a0f53;
}
.header small {
color: #666;
}
.header .active {
color: #2c7613;
}
4. Implement react router
In the next step, we have to implement a routing in the react application. We would recommend you to check this link to implement routing in the login application.
After implementing routing in the react app, your `App.js` file will look like this.
App.js
import React from 'react';
import { BrowserRouter, Switch, NavLink, Redirect, Route } from 'react-router-dom';
import Login from './pages/Login';
import Dashboard from './pages/Dashboard';
function App() {
return (
<div className="App">
<BrowserRouter>
<div>
<div className="header">
<NavLink activeClassName="active" to="/login">Login</NavLink>
<NavLink activeClassName="active" to="/dashboard">Dashboard</NavLink>
</div>
<div className="content">
<Switch>
<Route path="/login" component={Login} />
<Route path="/dashboard" component={Dashboard} />
<Redirect to="/login" />
</Switch>
</div>
</div>
</BrowserRouter>
</div>
);
}
export default App;
5. Add services to call API
In the upcoming steps, we have to call an API to implement authentication. If you don’t know about the API integration then refer to this link: API calls with React Hooks.
Let’s create services to manage an API call. We will divide these services in two parts.
-
Authentication services (`services/auth.js`)
The following services will be managed in this category.
- setAuthToken - Globally add/remove the access token to the axios header.
- verifyTokenService - It’s used on web page reload to verify the refresh token to generate a new access token if refresh token is present.
- userLoginService - Call the user login API to validate the user credential from the login component.
- userLogoutService - Manage the logout from the dashboard page.
-
User services (`services/user.js`)
- getUserListService - To get the list of users from the dashboard page. This service required an access token to get the data.
Here we will use the axios npm package so it will automatically read the XSRF token from the cookie and append it in the request of the API call to avoid the CSRF attack. If the XSRF token is not present in the request header then the private route won’t be accessible from the server.
services/auth.js
import axios from "axios";
axios.defaults.withCredentials = true;
const API_URL = 'http://localhost:4000';
// set token to the axios
export const setAuthToken = token => {
if (token) {
axios.defaults.headers.common['Authorization'] = `Bearer ${token}`;
}
else {
delete axios.defaults.headers.common['Authorization'];
}
}
// verify refresh token to generate new access token if refresh token is present
export const verifyTokenService = async () => {
try {
return await axios.post(`${API_URL}/verifyToken`);
} catch (err) {
return {
error: true,
response: err.response
};
}
}
// user login API to validate the credential
export const userLoginService = async (username, password) => {
try {
return await axios.post(`${API_URL}/users/signin`, { username, password });
} catch (err) {
return {
error: true,
response: err.response
};
}
}
// manage user logout
export const userLogoutService = async () => {
try {
return await axios.post(`${API_URL}/users/logout`);
} catch (err) {
return {
error: true,
response: err.response
};
}
}
services/user.js
import axios from "axios";
const API_URL = 'http://localhost:4000';
// get list of the users
export const getUserListService = async () => {
try {
return await axios.get(`${API_URL}/users/getList`);
} catch (err) {
return {
error: true,
response: err.response
};
}
}
6. Implement redux
We’ll assume that you already know about the redux and redux-thunk. If you don’t know how to set up a redux store then kindly refer to the following articles for more understanding.
If you have read the first article about the understanding of the authentication where we have explained the reason to use redux.
We’ll manage the authentication user details in the redux store along with the access token and expiry time to avoid the Cross-site scripting (XSS) attack and refresh token token and XSRF token will be managed in cookie to avoid Cross-site request forgery (CSRF/XSRF) attack.
In the current application, we’ll manage only the authentication reducer in the redux store. Check out the below state to be considered as an initial state in the auth reducer.
// define initial state of auth reducer
const initialState = {
token: null, // manage the access token
expiredAt: null, // manage expiry time of the access token
user: null, // manage the user details
authLoading: true, // to indicate that the auth API is in progress
isAuthenticated: false, // consider as a authentication flag
userLoginLoading: false, // to indicate that the user signin API is in progress
loginError: null // manage the error of the user signin API
}
For your understanding, we have written the short description within the code.
We’ll also consider the several types of actions for the redux store.
- VERIFY_TOKEN_STARTED - We can use it when the `verifyTokenService` API call will be started.
- VERIFY_TOKEN_END - We will make this call when the `verifyTokenService` process is over.
- USER_LOGIN_STARTED - We’ll use it when the `userLoginService` starts.
- USER_LOGIN_FAILURE - This action type will be used on failure of the `userLoginService` API call.
- VERIFY_USER_SUCCESS - This type will help us to manage the response on successful logged-in.
- USER_LOGOUT - Use it to manage the user logout functionality.
Let’s set up a redux in application by adding the several files.
actions/actionTypes.js
export const VERIFY_TOKEN_STARTED = 'VERIFY_TOKEN_STARTED';
export const VERIFY_TOKEN_END = 'VERIFY_TOKEN_END';
export const USER_LOGIN_STARTED = 'USER_LOGIN_STARTED';
export const USER_LOGIN_FAILURE = 'USER_LOGIN_FAILURE';
export const VERIFY_USER_SUCCESS = 'VERIFY_USER_SUCCESS';
export const USER_LOGOUT = 'USER_LOGOUT';
actions/authActions.js
import {
VERIFY_TOKEN_STARTED, VERIFY_USER_SUCCESS, VERIFY_TOKEN_END,
USER_LOGIN_STARTED, USER_LOGIN_FAILURE, USER_LOGOUT
} from "./actionTypes";
import { setAuthToken } from "../services/auth";
// verify token - start
export const verifyTokenStarted = (silentAuth = false) => {
return {
type: VERIFY_TOKEN_STARTED,
payload: {
silentAuth
}
}
}
// verify token - end/failure
export const verifyTokenEnd = () => {
return {
type: VERIFY_TOKEN_END
}
}
// user login - start
export const userLoginStarted = () => {
return {
type: USER_LOGIN_STARTED
}
}
// user login - failure
export const userLoginFailure = (error = 'Something went wrong. Please try again later.') => {
return {
type: USER_LOGIN_FAILURE,
payload: {
error
}
}
}
// verify token - success
export const verifyUserSuccess = ({ token, expiredAt, user }) => {
return {
type: VERIFY_USER_SUCCESS,
payload: {
token,
expiredAt,
user
}
}
}
// handle user logout
export const userLogout = () => {
setAuthToken();
return {
type: USER_LOGOUT
}
}
In the next steps, we’ll create async actions to manage verification of token, user login and user logout.
asyncActions/authAsyncActions.js
import {
verifyTokenStarted, verifyUserSuccess, verifyTokenEnd,
userLoginStarted, userLoginFailure, userLogout
} from "../actions/authActions";
import { verifyTokenService, userLoginService, userLogoutService } from '../services/auth';
// handle verify token
export const verifyTokenAsync = (silentAuth = false) => async dispatch => {
dispatch(verifyTokenStarted(silentAuth));
const result = await verifyTokenService();
if (result.error) {
dispatch(verifyTokenEnd());
if (result.response && [401, 403].includes(result.response.status))
dispatch(userLogout());
return;
}
if (result.status === 204)
dispatch(verifyTokenEnd());
else
dispatch(verifyUserSuccess(result.data));
}
// handle user login
export const userLoginAsync = (username, password) => async dispatch => {
dispatch(userLoginStarted());
const result = await userLoginService(username, password);
if (result.error) {
dispatch(userLoginFailure(result.response.data.message));
return;
}
dispatch(verifyUserSuccess(result.data));
}
// handle user logout
export const userLogoutAsync = () => dispatch => {
dispatch(userLogout());
userLogoutService();
}
Let’s create a reducer file to add it to the redux store.
reducers/authReducer.js
import {
VERIFY_TOKEN_STARTED, VERIFY_TOKEN_END,
USER_LOGIN_STARTED, USER_LOGIN_FAILURE,
VERIFY_USER_SUCCESS, USER_LOGOUT
} from "../actions/actionTypes";
// define initial state of auth reducer
const initialState = {
token: null, // manage the access token
expiredAt: null, // manage expiry time of the access token
user: null, // manage the user details
authLoading: true, // to indicate that the auth API is in progress
isAuthenticated: false, // consider as a authentication flag
userLoginLoading: false, // to indicate that the user signin API is in progress
loginError: null // manage the error of the user signin API
}
// update store based on type and payload and return the state
const auth = (state = initialState, action) => {
switch (action.type) {
// verify token - started
case VERIFY_TOKEN_STARTED:
const { silentAuth } = action.payload;
return silentAuth ? {
...state
} : initialState;
// verify token - ended/failed
case VERIFY_TOKEN_END:
return {
...state,
authLoading: false
};
// user login - started
case USER_LOGIN_STARTED:
return {
...state,
userLoginLoading: true
};
// user login - ended/failed
case USER_LOGIN_FAILURE:
const { error } = action.payload;
return {
...state,
loginError: error,
userLoginLoading: false
};
// verify token - success
case VERIFY_USER_SUCCESS:
const { token, expiredAt, user } = action.payload;
return {
...state,
token,
expiredAt,
user,
isAuthenticated: true,
authLoading: false,
userLoginLoading: false
}
// handle user logout
case USER_LOGOUT:
return {
...initialState,
authLoading: false
}
default:
return state
}
}
export default auth;
reducers/index.js
import { combineReducers } from 'redux';
import auth from './authReducer';
// to combine all reducers together
const appReducer = combineReducers({
auth
});
export default appReducer;
store.js
import { createStore, applyMiddleware, compose } from 'redux';
import thunk from 'redux-thunk';
import appReducer from './reducers';
const composeEnhancers =
typeof window === 'object' &&
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ ?
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({
// Specify extension’s options like name, actionsBlacklist, actionsCreators, serialize...
}) : compose;
const enhancer = composeEnhancers(
applyMiddleware(thunk),
// other store enhancers if any
);
export default createStore(
appReducer, enhancer
);
index.js
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import App from './App';
import store from './store';
import './index.css';
ReactDOM.render(
<React.StrictMode>
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>,
document.getElementById('root')
);
7. Create route guard
Here we’ll create two types of the route guards for managing the redirection. One will be used for private routes and another one will be used for public routes.
routes/PrivateRoute.js
import React from 'react';
import { Route, Redirect } from 'react-router-dom';
// handle the private routes
function PrivateRoute({ component: Component, ...rest }) {
return (
<Route
{...rest}
render={(props) => rest.isAuthenticated ? <Component {...props} /> : <Redirect to={{ pathname: '/login', state: { from: props.location } }} />}
/>
)
}
export default PrivateRoute;
routes/PublicRoute.js
import React from 'react';
import { Route, Redirect } from 'react-router-dom';
// handle the public routes
function PublicRoute({ component: Component, ...rest }) {
return (
<Route
{...rest}
render={(props) => !rest.isAuthenticated ? <Component {...props} /> : <Redirect to={{ pathname: '/dashboard' }} />}
/>
)
}
export default PublicRoute;
Here we used the `isAuthenticated` flag from the redux store to manage application routing.
8. Connect components to the redux store
It’s the last step to connect components to the redux store. We will use the `useSelector` & `useDispatch` hooks from the react redux.
App.js
import React, { useEffect } from 'react';
import { BrowserRouter, Switch, NavLink, Redirect } from 'react-router-dom';
import { useSelector, useDispatch } from 'react-redux';
import Login from './pages/Login';
import Dashboard from './pages/Dashboard';
import PrivateRoute from './routes/PrivateRoute';
import PublicRoute from './routes/PublicRoute';
import { verifyTokenAsync } from './asyncActions/authAsyncActions';
function App() {
const authObj = useSelector(state => state.auth);
const dispatch = useDispatch();
const { authLoading, isAuthenticated } = authObj;
// verify token on app load
useEffect(() => {
dispatch(verifyTokenAsync());
}, []);
// checking authentication
if (authLoading) {
return <div className="content">Checking Authentication...</div>
}
return (
<div className="App">
<BrowserRouter>
<div>
<div className="header">
<NavLink activeClassName="active" to="/login">Login</NavLink>
<NavLink activeClassName="active" to="/dashboard">Dashboard</NavLink>
</div>
<div className="content">
<Switch>
<PublicRoute path="/login" component={Login} isAuthenticated={isAuthenticated} />
<PrivateRoute path="/dashboard" component={Dashboard} isAuthenticated={isAuthenticated} />
<Redirect to={isAuthenticated ? '/dashboard' : '/login'} />
</Switch>
</div>
</div>
</BrowserRouter>
</div>
);
}
export default App;
List of changes implemented in the `App` component.
- We have used react redux hooks to manage the auth object of the store.
- Implemented route guards with the help of auth object using redux store.
- We have called the service to verify the token on app load.
- We have prevented the component from rendering while checking authentication on page reload.
- We have also managed the default redirect if the route is not matched.
Now let’s talk about the `Login` component.
pages/Login.js
import React, { useState } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { userLoginAsync } from './../asyncActions/authAsyncActions';
function Login() {
const authObj = useSelector(state => state.auth);
const dispatch = useDispatch();
const { userLoginLoading, loginError } = authObj;
const username = useFormInput('');
const password = useFormInput('');
// handle button click of login form
const handleLogin = () => {
dispatch(userLoginAsync(username.value, password.value));
}
return (
<div>
Login<br /><br />
<div>
Username<br />
<input type="text" {...username} autoComplete="new-password" />
</div>
<div style={{ marginTop: 10 }}>
Password<br />
<input type="password" {...password} autoComplete="new-password" />
</div>
<input
type="button"
style={{ marginTop: 10 }}
value={userLoginLoading ? 'Loading...' : 'Login'}
onClick={handleLogin}
disabled={userLoginLoading} />
{loginError && <div style={{ color: 'red', marginTop: 10 }}>{loginError}</div>}
</div>
);
}
// custom hook to manage the form input
const useFormInput = initialValue => {
const [value, setValue] = useState(initialValue);
const handleChange = e => {
setValue(e.target.value);
}
return {
value,
onChange: handleChange
}
}
export default Login;
List of changes implemented in the `Login` component.
- We have connected the redux store with the component.
- Called the user login service on button click of the login form.
- Handle the response messages using redux store.
So at last, we will work on the `Dashboard` component.
pages/Dashboard.js
import React, { useEffect, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import moment from 'moment';
import { verifyTokenAsync, userLogoutAsync } from "./../asyncActions/authAsyncActions";
import { userLogout, verifyTokenEnd } from "./../actions/authActions";
import { setAuthToken } from './../services/auth';
import { getUserListService } from './../services/user';
function Dashboard() {
const dispatch = useDispatch();
const authObj = useSelector(state => state.auth);
const { user, token, expiredAt } = authObj;
const [userList, setUserList] = useState([]);
// handle click event of the logout button
const handleLogout = () => {
dispatch(userLogoutAsync());
}
// get user list
const getUserList = async () => {
const result = await getUserListService();
if (result.error) {
dispatch(verifyTokenEnd());
if (result.response && [401, 403].includes(result.response.status))
dispatch(userLogout());
return;
}
setUserList(result.data);
}
// set timer to renew token
useEffect(() => {
setAuthToken(token);
const verifyTokenTimer = setTimeout(() => {
dispatch(verifyTokenAsync(true));
}, moment(expiredAt).diff() - 10 * 1000);
return () => {
clearTimeout(verifyTokenTimer);
}
}, [expiredAt, token])
// get user list on page load
useEffect(() => {
getUserList();
}, []);
return (
<div>
Welcome {user.name}!<br /><br />
<input type="button" onClick={handleLogout} value="Logout" /><br /><br />
<input type="button" onClick={getUserList} value="Get Data" /><br /><br />
<b>User List:</b>
<pre>{JSON.stringify(userList, null, 2)}</pre>
</div>
);
}
export default Dashboard;
List of changes implemented in the `Dashboard` component.
- We have read the auth object form the redux store to manage the authenticated user details.
- By default we called an API to get the user list on page load.
- We have also used a button to obtain a user list by clicking on it.
- We have also handled the click event of the logout button and called the service to manage the user signout.
- The most important thing is we have set the timer on page load to renew/regenerate the new access token before it expires. For that we have used the moment npm package.
9. Output
Let’s run this project and see the output. You have to run the node server that we describe in the previous article to enable the API.
Output - Login App with CSRF protection - Implement authentication in ReactJS using secure REST API - Clue Mediator