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
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.
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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 | 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 | 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | 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.
1 2 3 4 5 6 7 8 9 10 | // 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
1 2 3 4 5 6 7 8 | 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 | 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 | 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 | 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
1 2 3 4 5 6 7 8 9 | import { combineReducers } from 'redux'; import auth from './authReducer'; // to combine all reducers together const appReducer = combineReducers({ auth }); export default appReducer; |
store.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 | 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 | 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 | 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.
What keeps someone from sniffing username and password in axios.post(
${API_URL}/users/signin
, { username, password }); ?Hello Thomas,
If it’s easily possible then no one will use REST API. If you have any idea about it then please share with us.
We will enjoy learning new things. 🙂
Thank you!
Thanks for the response. And also thanks very much for all these articles! After doing some more reading I guess the best answer to my question would be to make sure to use https, that way the username and password would be encrypted going across the wire.
Sure, We will also check it and share it if we find the best solution.
As the security related infos are basically stored inside Redux store, the token only persists as long as the session (bowser) is running. However, when the browser is closed, all the infos are gone thus, If I want to make ‘stayed sign in’ feature, do I have to store the Token info inside Cookies as well as helper functions or other means to refresh the token outside of the browser
Hey Col,
You can see here, we are storing the refresh token in cookies to perform the silent authentication.
So when we are reloading the page then we will use these all details to get the basic user information before rendering the component. Please check out the working demo and output for more details.
Feel free to share your thoughts.
Subscribe us for weekly updates or like and follow us for regular updates.
Happy Coding..!!
This post helps a lot. I have two questions
1 – How to handle verifyTokenTimer() commonly when we have more pages. As we are making repetitive code in each file.
2 – When I am calling login it gives me a response in the Network tab with Set-Cookie but not in COOKIE inside the application tab.
Hello Romil,
Please find your answer below.
Ans1 – You can use that method in the root level of the private route or you can create a custom effect and use it at multiple places.
Ans2 – Can you please clone the repository that we have provided and check it in your local system?
Subscribe us for weekly updates or like and follow us for regular updates.
Thank you!
Hey, great post! Thanks for that but i have question. As it is described at OWASP recomendation: https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#use-of-custom-request-headers
“CSRF tokens should not be transmitted using cookies.”
So how safe is to send it via cooke from server and then send it back to server also as a cookie? (I know it’s in header but its still in cookie also)
Hi Szymon, As we researched on CSRF tokens, we can’t find a way to manage CSRF tokens without cookies in SPA.
If you have any feedback for this approach (good and bad), do let us know in the comments. So we can share it with all other readers.
Thank you!
Hi,
I’m so grateful for your detailed and complete article.
In “asyncActions/authAsyncActions.js” line 20, when we get status code 204 it means that the browser didn’t send the refresh token to server (maybe user deleted cookies or sth else). In this situation, why we don’t logout the user just like when we get 401 or 403 status codes?
Hi Moj, For the status code 204, we don’t need to execute the logout method because we verify the token on page load if exists.
Hello
The concepts and explanations of secure rest api in your article are fantastic and I really appriciate that. I know that the main goal of this article is implementation of an acceptable authentication but there is a risk of infinite re-render loop in your react codes:
Let’s say that after signing in, we render the dashboard component and call getUsersList on page load. And let’s assume that there is a problem in our request and we get status error 400 (bad request). In this case we get result.error in line 28 of dashboard.js and we only dispatch verifyTokenEnd but we don’t logout. Consequently, we change “authLoading” state and this change causes App component to re-render. App component re-renders and as a result dashboard component renders again. So getUserList gets called again and we get error 400 again and the loop continues infinitly.
As I know in react when we change the state of a parent component in it’s child we cause an infinite loop of re-rendering. This is because of the fact that when a parent component re-renders, it’s child components renders again as the first time and dependency array of useEffect in child component is useless in this case.
This is All I know, but I do not know how to resolve this problem in your code!!! Is it OK to move line 28 (in dashboard.js) inside of “IF” block (in line 29) just before logging out? And then after the “IF” block we manage the 400 error code independently? So in this case, If we get error 400 we don’t dispatch verifyTokenEnd. Is this a good solution?
Hi Mary, I will suggest you to manage the HTTP interceptor for your API request where you can handle all API request-response. But it’s not a good idea to perform the logout operation on status code 400.
Let us know if you have any queries.
Hello,
Thank you for the great article. I do have a question about the access token.
It’s clear that the access token is being saved in a state variable and not on the browser. If I do a hard refresh on the page, how would this affect the values that are saved in the state vars?
Would I be able to generate a new access token from the refresh token that is stored in the cookie section without providing the actual access token?
Also wouldn’t be better if I do an interceptor to refresh the token automatically once it’s expired?
Hi Codingride,
Yes, you have to use the refresh token to generate the access token and yes it will be good to refresh the token automatically once it’s expired (You can toggle this functionality by selecting the Remember Me checkbox).
I don’t know why but when login and moved to dashboard page and refreshed and it again back to login why? i follow whole tutorial.
Can you please clone the GitHub repository and compare your code with it?