Refresh Auth Token Rotation (Node
js & React ) — Part 2
Tékos Bence - Follow
12min read - Mar 18, 2024
¥ 7
Ss -
Auth Token Rotation ( Node js & React js )
Welcome back! In this section, we'll create a small frontend demo to test the
token rotation solution we implemented in the previous part. Let’s start by
creating a new React app.
We create a React app with vite:apm create
And install the axios:
apm install axios
We'll also install React Redux because it makes token handling much easier:
apm instal
apm install
Introduction to Redux Store:
Redux is a state management library commonly used with React applications
to manage application state in a predictable and centralized manner. The
Redux store serves as a single source of truth for the entire application state,
allowing components to access and update state consistently across the
application. It helps simplify complex data flow and makes it easier to manage
application state, especially in large-scale applications with many components.
Let’s implement the redux store what we will use in our little demo. Make a
folder store and create two files: authSlice and store.
Here is the authSlice:import { createSlice } from
Import jwt decode from "jwt-decode”
const initialstate = {
token: null,
pull,
authTokenSlice = createSlice ((
addToken (state, action)
action.payload;
export const authTokenActions
export default authTokens:
thTokenSlice.actions;
In this code snippet, we’re defining a Redux slice named authTokenSlice using
createSlice from the Redux Toolkit. This slice manages the authentication-
related state in our Redux store. The initial state includes properties for the
authentication token (token), user ID (userld). The slice contains reducer
functions to add or delete the authentication token from the state. When
adding a token, jwt_decode is used to extract the user ID from the JWT token
payload, which is then stored in the state. This slice provides actions like
addToken and deleteToken, which can be dispatched to update the
authentication state in the Redux store.And here is the store:
mport { configure’
import authTokensL
//Pexs:
mport
mport {
onfigurestore (
authTokel 1e.reducer) ,
In this code snippet, we're configuring the Redux store using configureStore
from the Redux Toolkit. We’re also setting up state persistence using redux-
persist to persist the authToken slice’s state across browser sessions. The
authTokenPersistConfig defines the configuration for persisting the
authentication token slice using sessionStorage. This ensures that the
authentication state persists even after the user closes and reopens the
browser. Finally, we export the configured Redux store and persistor for use
throughout our application. This setup provides a robust and reliable
mechanism for managing authentication state in our React application.
Now we need to use it so let’s add to our main,jsx:import React from ‘react!
import Rea
mport
M from 'react-dom/client'
mport {
import {
import {
mport
stGate ) from "redux-p. Jinvegration/react";
Re:
DOM. createRoot (document .getELlementById ("root") ) . render (
In this code snippet, we’re setting up the entry point for our React application.
First, we import necessary dependencies such as React, ReactDOM, App
component, and stylesheets. We also import BrowserRouter from “react-
router-dom” to enable client-side routing in our application. Next, we import
our Redux store and persistor from “./store/store” to provide centralized state
management across the application. We then import Provider from “react-
redux” to wrap our entire application with the Redux store, allowing
components to access the Redux state. Additionally, we import PersistGate
from “redux-persist/integration/react” to ensure that our Redux store is
rehydrated with persisted state before rendering the application. Finally, we
use ReactDOM.createRoot to render our application into the root element in
the HTML document. Within the render method, we wrap our entire
application with Provider and PersistGate, ensuring that the Redux store and
persisted state are available throughout the component tree. Inside the
Provider, we nest BrowserRouter to enable routing within our application, with
the App component serving as the root component. Overall, this setup ensuresseamless integration of Redux state management and client-side routing in our
React application, providing a smooth and responsive user experience.
Alright, now that we've configured the Redux store and routing, it’s time to
create our pages. We'll start by setting up a pages folder where we'll
implement our pages. Given that this tutorial focuses on token rotation, we'll
create just three pages: a main protected page, a login page, and a sign-up page.
First we implement the login pag
import React, { useState } from
import AxiosInstance from "../axios/axiosInstance";
import { useDispatch } from "react-redux";
okenActions } from "
« /store/a\
import {
import { useNavigate } from "react-router-don";
const Login = () =)
const [loginData, setLoginData] = uses
passwort
Me
const [errors, setBrror
const dispatch = useDispateh ();
const navigate = useNavigate();
const AxiosInstance ({
“Content~Type": "application/json",
utChangeHiandle = (e
const { name, value } ~ event
setLoginData({ ...loginData,
email") {
vemail = "5
: value J);
if (name
}
if (name === “password") {
errozs.password = "";
}
const onk
n= async (e) =>
e.preventDefault ();
const validationErrors = validateForm(loginData) ;
if (Object keys (validationBrrors) length === 0) {
console. log(loginData) +await axiosInstance
-post ("/users/signin", loginData)
-then( (response) => (
Lf (response.data.accessToken) (
dispatch (authTokenActions.addToken (response .data.accessToken) ) +
navigate ("/");
,
»
scatch( (error) => {
console. log (error);
De
} else {
setErrors (validationtrrors);
const validateForm = (data) => {
const errors = {};
if (Idata.password) {
exrors.password =
}
if (Jdatavemaily {
Password is required";
errors.email = "Email is required";
} else if (JisValidamail (data-email)) {
errors.email = "Invalid email format";
}
return errors;
be
const isValidfmail = (email) => {
const emailpattern = /*[a-zA-20-9._-1+8[a~2A~20-9.-]+\. [a-zA~2] (2,418/¢
// Test the email against the pattern
retuzn emailPattern.test (email);
ie
return (
(errors.password}
n
16px", display: "flex", flexDirection:
column") }>
export default Signtp:
This code defines a React component called SignUp, serving as the gateway for
users to create new accounts within our application. It harnesses the power of
React hooks, such as useState, to manage component state, and useDispatch to
dispatch actions to our Redux store. As users input their desired email and
password, the onInputChangeHandle function dynamically updates the
component's state, while also performing essential form validation to ensure
data integrity. Upon form submission, the onAuth function orchestrates the
validation process, sending a POST request to the “/users/signUp” API
endpoint via Axios. If the authentication is successful, the response data —
including an access token — is stored in the Redux store using dispatch.
Furthermore, users are gracefully navigated to the home page (“/”) using the
useNavigate hook from React Router. In summary, this component seamlessly
integrates account creation functionality into our application, offering users a
secure and intuitive signup experience.
As mentioned earlier, we're developing a third secure view. Before beginning
the implementation of this view, it’s essential to create a private secure route
component.
import React from "react";
mport { Outlet, Navigate } from "react-router-dom";
mport. { useSelector } from "r
redux";
> state.authToken. token);
login" />;
return authToken ? export default PrivateRoute;
In this code, we're defining a React component called PrivateRoute, which
‘ion.
serves as a wrapper for routes that require authentication in our applic:
The component utilizes React Router’s Outlet and Navigate components for
routing and useSelector hook from react-redux for accessing the Redux store.
Inside the PrivateRoute component, we extract the authToken from the Redux
store using useSelector. The authToken represents the user’s authentication
token, indicating whether the user is authenticated or not.
Next, we use a ternary operator to conditionally render the Outlet component
if authToken exists, implying that the user is authenticated and should have
access to the protected route. The Outlet component is a placeholder provided
by React Router for rendering child routes within the parent route.
If authToken does not exist (i.e., the user is not authenticated), we render the
Navigate component, which redirects the user to the login page (“/login”).
Navigate is a component provided by React Router for declarative navigation.
Overall, this PrivateRoute component acts as a guard for protected routes in
our application, ensuring that only authenticated users can access them. If an
unauthenticated user tries to access a protected route, they are automatically
redirected to the login page. This enhances the security and user experience of
our application by enforcing authentication requirements for sensitive routes.
Now, we can create the main page:import React, { useState } from "zeact";
import { useDispatch, useSelector } from "react-redu:
import { authTokenActions } from "../store/authSlice’
import { useNavigate } from "react-router-dom";
import Axiostnstance from "../axios/axiosinstance";
const Main = () => {
const dispatch = useDispateh ();
const navigate = useNavigate ();
const userid = useSelector( (state) => state.authToken.userid) ;
const [useNane, setUsername] = useState("");
const axiesInstance = AxiosInstance({
“content~Type": "application/json"
De
const onLogout = asyne ()
await axiosInstance
-get ("/users/logout!
then ( (response)
")
‘
console. leg (response) ;
dispatch (authTokenActions.deleteToken ()) +
navigate ("/login")
»
scatch( (error) => {
console. log (error);
De
b
const getUserData = async () => {
await axiosInstance
«get ("/users/getUser/" + userid)
-then( (response) => {
console. log (response)
setUsername (response .data-userEmail) ;
»
seatch ((error) => {
console. log (error);
ye
ie
return (
Protected Main
Logout
"G6
(userName}
}
export default Main
In this code, we have a React component called Main, responsible for
rendering the main page of our application. This component includes
functionality for logging out the user and fetching user data. Let’s break down
the key components:
1. useState and useSelector:
* We use the useState hook to manage the local state of userName,
representing the user’s name.
« The useSelector hook is utilized to access the userId from the Redux store,
retrieved using the authToken slice.
2. useDispatch and authTokenActions:
* useDispatch hook is used to get the Redux dispatch function, enabling us to
dispatch actions to the Redux store.
* authTokenActions contains action creators for managing authentication-
related actions, such as adding or deleting authentication tokens.
3. useNavigate:
+ useNavigate hook from React Router DOM is employed for declarative
navigation within the application.
4. AxiosInstance:+ AxiosInstance is a configured instance of Axios, a library for making HTTP
requests. It’s set up to send requests with a specific content type of
“application/json”.
5. onLogout Function:
* This function is triggered when the user clicks the “LogOut” button. It
sends a GET request to “/users/logout” endpoint using AxiosInstance,
logging out the user on the server-side. Upon successful response, it
dispatches the deleteToken action to remove the authentication token from
the Redux store and navigates the user to the login page.
6. getUserData Function:
+ This function fetches user data from the server by sending a GET request to
“/users/getUser/{userld}” endpoint. Upon receiving a response, it sets the
userName state to the retrieved user’s email. This function is invoked when
the user clicks the “Get User Data” button.
7. Rendering:
+ The render function displays a message indicating that the page is
protected.
+ Two buttons are provided: one for logging out and the other for fetching
user data.
¢ Ifthe userName state is not empty (i-e., user data has been fetched
successfully), the user’s email is displayed on the page.
Now, let’s add the routes to the app,jsx file:import './App.
import { Route:
mport Login from *./pack
SignUp fret
mport Main from './pages/MainProtected";
import PrivateRoute from '../utils,
-router-don";
s/Signlp';
function Ape)
return (
}/>
}>
element={)}/>
The Routes component from react-router-dom is used to define the
application’s routes. Each Route element specifies a path and the
corresponding component to render. The PrivateRoute component ensures
that certain routes are only accessible to authenticated users. Inside the
Routes component, Login and SignUp pages are rendered directly, while the
Main page is wrapped inside the PrivateRoute component to restrict access to
authenticated users only. Overall, this App component sets up the application’s
routing system, directing users to the appropriate pages based on the requested
URLs.
Let me explain how it works!
To run the frontend, navigate to the frontend folder and:apm run dev
To run the backend, navigate to the backend folder and:
node se:
First you create an account:
Create your account!
Sign Up page
After logging in or signing up, you will be directed to the main page:Main page
Because we've set the access token lifetime to 10 seconds, after this period, the
access token expires. Attempting to retrieve user data results in a 403
Forbidden error due to the expired access token. This issue involves our Axios
instance, as upon encountering the Forbidden error, we send a request with
our refresh token from the cookies. Subsequently, we generate a new access
token, pass it to the original request, and successfully retrieve the user data. If
the refresh token also expires, the user will be logged out and prompted to log
in again to create new refresh and access tokens.Frontend Backend
Request with the acsessoken
@>{o
|
| ester en
lens
‘Repote he eign request wit the new ceass akon
Atbitecture
page after get new access tokenThis process may seem complex initially, but it’s simpler than you might
expect. Remember, you can adjust the access token lifetime, but it should
always be shorter than the refresh token’s lifetime.
Lhope you found this tutorial enjoyable and informative. If you want to verify
and ensure that everything is done correctly, please visit my GitHub repository
for this code. Don’t forget to check out my other stories!
See you soon!
The Road to React: Your journey to master plain yet pragmatic React 2020th Edition Robin Wieruch - The special ebook edition is available for download now
The Road to React: Your journey to master plain yet pragmatic React 2020th Edition Robin Wieruch - The special ebook edition is available for download now