0% found this document useful (0 votes)
33 views

Refresh Auth Token Rotation (Node Js & React) - Part 2

Refresh Auth Token Rotation (Node js & React ) - Part 2

Uploaded by

Ronny Camacho
Copyright
© © All Rights Reserved
Available Formats
Download as PDF or read online on Scribd
0% found this document useful (0 votes)
33 views

Refresh Auth Token Rotation (Node Js & React) - Part 2

Refresh Auth Token Rotation (Node js & React ) - Part 2

Uploaded by

Ronny Camacho
Copyright
© © All Rights Reserved
Available Formats
Download as PDF or read online on Scribd
You are on page 1/ 21
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 ensures seamless 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 (

Welcome!

(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 token This 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!

You might also like