In this blog post, we'll explore the seamless integration of JWT authentication with React and react-router. We'll also learn how to handle public routes, secure authenticated routes, and utilize the axios library to make API requests with the authentication token.
Create a React Project
The following command will create a react project for us.
npm create vite@latest react-auth-demo
We will choose React
as our framework and JavaScript
as a variant.
To begin working on our project, we need to ensure that all dependencies are properly installed. We can achieve this by running
npm install
within our project directory. Once the installation process is complete, we can launch our React project using the command
npm run dev
Let's take these necessary steps to get our React project up and running smoothly.
Installing Dependencies for React-Router v6 and Axios
Before we proceed, let's ensure we have the necessary dependencies installed for our project. We'll start by installing react-router v6, which will handle routing within our React application. Additionally, we'll install Axios, a powerful library used for making API requests. By following these steps, we'll be equipped with the tools needed to implement seamless routing and perform efficient API communication. Let's begin by installing these dependencies.
npm install react-router-dom axios
Creating the AuthProvider and AuthContext in React
With our project set up and dependencies installed, we're ready to take the next step in implementing JWT authentication. In this section, we'll create an AuthProvider
component and an associated AuthContext
. These will enable us to store and share authentication-related data and functions throughout our application
In the following code snippet, we'll create the authProvider.js
file located at src > provider > authProvider.js
. Let's explore the implementation of the AuthProvider
and AuthContext
.
-
Import the necessary modules and packages:
- axios is imported from the "axios" package to handle API requests.
- createContext, useContext, useEffect, useMemo, and useState are imported from the "react" library.
import axios from "axios"; import { createContext, useContext, useEffect, useMemo, useState, } from "react";
-
Create an authentication context using createContext():
- createContext() creates an empty context object that will be used to share the authentication state and functions between components.
const AuthContext = createContext();
-
Create the AuthProvider component:
- This component serves as the provider for the authentication context.
- It receives children as a prop, which represents the child components that will have access to the authentication context.
const AuthProvider = ({ children }) => { // Component content goes here };
-
Define the
token
state usinguseState()
:-
token
represents the authentication token. -
localStorage.getItem("token")
retrieves the token value from the local storage if it exists.
const [token, setToken_] = useState(localStorage.getItem("token"));
-
-
Create the
setToken
function to update the authentication token:- This function is used to set the new token value.
- It updates the
token
state usingsetToken_()
and stores the token value in the local storage usinglocalStorage.setItem()
.
const setToken = (newToken) => { setToken_(newToken); };
-
Use
useEffect()
to set the default authorization header in axios and stores the token value in the local storage usinglocalStorage.setItem()
:- This effect runs whenever the
token
value changes. - If the
token
exists, it sets the authorization header in axios and localStorage. - If the
token
is null or undefined, it removes the authorization header from axios and localStorage.
useEffect(() => { if (token) { axios.defaults.headers.common["Authorization"] = "Bearer " + token; localStorage.setItem('token',token); } else { delete axios.defaults.headers.common["Authorization"]; localStorage.removeItem('token') } }, [token]);
- This effect runs whenever the
-
Create the memoized context value using useMemo():
- The context value includes the token and setToken function.
- The token value is used as a dependency for memoization.
const contextValue = useMemo( () => ({ token, setToken, }), [token] );
-
Provide the authentication context to the child components:
- Wrap the children components with the AuthContext.Provider.
- Pass the contextValue as the value prop of the provider.
return ( <AuthContext.Provider value={contextValue}> {children} </AuthContext.Provider> );
-
Export the useAuth hook for accessing the authentication context:
- useAuth is a custom hook that can be used in components to access the authentication context.
export const useAuth = () => { return useContext(AuthContext); };
-
Export the AuthProvider component as the default export:
- This allows other files to import and use the AuthProvider component as needed.
export default AuthProvider;
Complete Code:
import axios from "axios";
import { createContext, useContext, useEffect, useMemo, useState } from "react";
const AuthContext = createContext();
const AuthProvider = ({ children }) => {
// State to hold the authentication token
const [token, setToken_] = useState(localStorage.getItem("token"));
// Function to set the authentication token
const setToken = (newToken) => {
setToken_(newToken);
};
useEffect(() => {
if (token) {
axios.defaults.headers.common["Authorization"] = "Bearer " + token;
localStorage.setItem('token',token);
} else {
delete axios.defaults.headers.common["Authorization"];
localStorage.removeItem('token')
}
}, [token]);
// Memoized value of the authentication context
const contextValue = useMemo(
() => ({
token,
setToken,
}),
[token]
);
// Provide the authentication context to the children components
return (
<AuthContext.Provider value={contextValue}>{children}</AuthContext.Provider>
);
};
export const useAuth = () => {
return useContext(AuthContext);
};
export default AuthProvider;
In summary, this code sets up the authentication context using React's context API. It provides the authentication token and the setToken function to child components through the context. It also ensures that the default authorization header in axios is updated with the authentication token whenever it changes.
Creating Routes in React for JWT Authentication
To organize our routes effectively, we'll create a dedicated src > routes
folder. Inside this folder, we'll create an index.jsx
file, which will serve as the entry point for defining our application's routes. By structuring our routes in a separate folder, we can maintain a clear and manageable routing structure. Let's proceed with creating our routes and explore how we can incorporate JWT authentication into our React application.
Creating a ProtectedRoute Component for Authenticated Routes
In order to secure our authenticated routes and prevent unauthorized access, we'll create a dedicated component called ProtectedRoute
. This component will serve as a wrapper for our authenticated routes, ensuring that only authenticated users can access them. By implementing this component, we can easily enforce authentication requirements and provide a seamless user experience. Let's create the ProtectedRoute.jsx
file located at src > routes > ProtectedRoute.jsx
and enhance the security of our application.
-
We start by importing the necessary dependencies from the react-router-dom library:
import { Navigate, Outlet } from "react-router-dom"; import { useAuth } from "../provider/authProvider";
-
We define the ProtectedRoute component, which will serve as a wrapper for our authenticated routes:
export const ProtectedRoute = () => { const { token } = useAuth(); // Check if the user is authenticated if (!token) { // If not authenticated, redirect to the login page return <Navigate to="/login" />; } // If authenticated, render the child routes return <Outlet />; };
Inside the ProtectedRoute component, we access the token from the useAuth custom hook provided by the AuthContext. This hook allows us to retrieve the authentication token stored in the context.
We then check if the token exists. If the user is not authenticated (token is falsy or null), we use the Navigate component from react-router-dom to redirect the user to the login page ("/login").
If the user is authenticated, we render the child routes using the Outlet component. The Outlet component acts as a placeholder that displays the child components defined in the parent route.
In summary, the ProtectedRoute component serves as a guard for authenticated routes. If the user is not authenticated, they will be redirected to the login page. If the user is authenticated, the child routes defined within the ProtectedRoute component will be rendered using the Outlet component.
This code allows us to easily protect specific routes and control access based on the user's authentication status, providing a secure navigation experience in our React application.
Deep dive into Routes
Now that we have our ProtectedRoute
component and authentication context in place, we can proceed with defining our routes. By differentiating between public routes, authenticated routes, and routes for non-authenticated users, we can effectively handle navigation and access control based on JWT authentication. In this code snippet, we'll dive into the src > routes > index.jsx
file and explore how we can incorporate JWT authentication into our routing structure. Let's get started!
-
Import necessary dependencies:
- RouterProvider and createBrowserRouter are components imported from the react-router-dom library. They are used for configuring and providing the routing functionality.
- useAuth is a custom hook imported from "../provider/authProvider". It allows us to access the authentication context.
- ProtectedRoute is a component imported from "./ProtectedRoute". It serves as a wrapper for authenticated routes.
import { RouterProvider, createBrowserRouter } from "react-router-dom"; import { useAuth } from "../provider/authProvider"; import { ProtectedRoute } from "./ProtectedRoute";
-
Define the Routes component:
- This functional component acts as the entry point for configuring the application routes.
const Routes = () => { const { token } = useAuth(); // Route configurations go here };
-
Access the authentication token using the useAuth hook:
- The useAuth hook is called to retrieve the token value from the authentication context. It allows us to access the authentication token within the Routes component.
const { token } = useAuth();
-
Define routes accessible to all users:
- The routesForPublic array contains route objects that can be accessed by all users. Each route object consists of a path and an element.
- The path property specifies the URL path for the route, and the element property contains the JSX element/component to render.
const routesForPublic = [ { path: "/service", element: <div>Service Page</div>, }, { path: "/about-us", element: <div>About Us</div>, }, ];
-
Define routes accessible only to authenticated users:
- The routesForAuthenticatedOnly array contains route objects that can be accessed only by authenticated users. It includes a protected root route ("/") wrapped in the ProtectedRoute component and additional child routes defined using the children property.
const routesForAuthenticatedOnly = [ { path: "/", element: <ProtectedRoute />, children: [ { path: "/", element: <div>User Home Page</div>, }, { path: "/profile", element: <div>User Profile</div>, }, { path: "/logout", element: <div>Logout</div>, }, ], }, ];
-
Define routes accessible only to non-authenticated users:
- The routesForNotAuthenticatedOnly array contains route objects that are accessible only to non-authenticated users. It includes a login route ("/login").
const routesForNotAuthenticatedOnly = [ { path: "/", element: <div>Home Page</div>, }, { path: "/login", element: <div>Login</div>, }, ];
-
Combine and conditionally include routes based on authentication status:
- The createBrowserRouter function is used to create the router configuration. It takes an array of routes as its argument.
- The spread operator (...) is used to merge the route arrays into a single array.
- The conditional expression (!token ? routesForNotAuthenticatedOnly : []) checks if the user is authenticated (token exists). If not, it includes the routesForNotAuthenticatedOnly array; otherwise, it includes an empty array.
const router = createBrowserRouter([ ...routesForPublic, ...(!token ? routesForNotAuthenticatedOnly : []), ...routesForAuthenticatedOnly, ]);
-
Provide the router configuration using RouterProvider:
- The RouterProvider component wraps the router configuration, making it available for the entire application.
return <RouterProvider router={router} />;
Complete Code:
import { RouterProvider, createBrowserRouter } from "react-router-dom";
import { useAuth } from "../provider/authProvider";
import { ProtectedRoute } from "./ProtectedRoute";
const Routes = () => {
const { token } = useAuth();
// Define public routes accessible to all users
const routesForPublic = [
{
path: "/service",
element: <div>Service Page</div>,
},
{
path: "/about-us",
element: <div>About Us</div>,
},
];
// Define routes accessible only to authenticated users
const routesForAuthenticatedOnly = [
{
path: "/",
element: <ProtectedRoute />, // Wrap the component in ProtectedRoute
children: [
{
path: "/",
element: <div>User Home Page</div>,
},
{
path: "/profile",
element: <div>User Profile</div>,
},
{
path: "/logout",
element: <div>Logout</div>,
},
],
},
];
// Define routes accessible only to non-authenticated users
const routesForNotAuthenticatedOnly = [
{
path: "/",
element: <div>Home Page</div>,
},
{
path: "/login",
element: <div>Login</div>,
},
];
// Combine and conditionally include routes based on authentication status
const router = createBrowserRouter([
...routesForPublic,
...(!token ? routesForNotAuthenticatedOnly : []),
...routesForAuthenticatedOnly,
]);
// Provide the router configuration using RouterProvider
return <RouterProvider router={router} />;
};
export default Routes;
Final Integration
Now that we have our AuthContext
, AuthProvider
and Routes
ready, let's integrate them together in App.jsx
.
-
Import the necessary components and files:
-
AuthProvider
is a component imported from"./provider/authProvider"
. It provides the authentication context to the application. -
Routes
is a component imported from"./routes"
. It defines the application routes.
import AuthProvider from "./provider/authProvider"; import Routes from "./routes";
-
-
Wrap the
Routes
component with theAuthProvider
component:- The
AuthProvider
component is used to provide the authentication context to the application. It wraps around theRoutes
component to make the authentication context available to all components within theRoutes
component tree.
return ( <AuthProvider> <Routes /> </AuthProvider> );
- The
Complete Code:
import AuthProvider from "./provider/authProvider";
import Routes from "./routes";
function App() {
return (
<AuthProvider>
<Routes />
</AuthProvider>
);
}
export default App;
Now that everythings is in place, its time to implement Login
and Logout
.
Let's create a file for Login Page src > pages > Login.jsx
Login Page
const Login = () => {
const { setToken } = useAuth();
const navigate = useNavigate();
const handleLogin = () => {
setToken("this is a test token");
navigate("/", { replace: true });
};
setTimeout(() => {
handleLogin();
}, 3 * 1000);
return <>Login Page</>;
};
export default Login;
- The Login component is a functional component that represents the login page.
- It imports the setToken function from the authentication context using the useAuth hook.
- The navigate function from the react-router-dom library is imported to handle navigation.
- Inside the component, there is a handleLogin function that sets a test token using the setToken function from the context and navigates to the home page ("/") with the replace option set to true.
- A setTimeout function is used to simulate a delay of 3 seconds before calling the handleLogin function.
- The component returns the JSX for the login page, which in this case is a placeholder text.
Now we will create a file for Logout Page src > pages > Logout.jsx
Logout Page
import { useNavigate } from "react-router-dom";
import { useAuth } from "../provider/authProvider";
const Logout = () => {
const { setToken } = useAuth();
const navigate = useNavigate();
const handleLogout = () => {
setToken();
navigate("/", { replace: true });
};
setTimeout(() => {
handleLogout();
}, 3 * 1000);
return <>Logout Page</>;
};
export default Logout;
- In Logout component, we call
setToken
function with no arguments, with is equal tosetToken(null)
.
Now, we will replace the existing Login and Logout components in the Routes component with the updated versions.
const routesForNotAuthenticatedOnly = [
{
path: "/",
element: <div>Home Page</div>,
},
{
path: "/login",
element: <Login />,
},
];
In the routesForNotAuthenticatedOnly
array, the element
property of "/login"
is set to <Login />
, which means that the Login
component will be rendered when the user visits the "/login" path.
const routesForAuthenticatedOnly = [
{
path: "/",
element: <ProtectedRoute />, // Wrap the component in ProtectedRoute
children: [
{
path: "/",
element: <div>User Home Page</div>,
},
{
path: "/profile",
element: <div>User Profile</div>,
},
{
path: "/logout",
element: <Logout />,
},
],
},
];
In the routesForAuthenticatedOnly
array, the element
property of "/logout"
is set to <Logout />
, which means that the Logout component will be rendered when the user visits the "/logout" path.
Test Flow
- When you first visit the root page
/
, you will see the "Home Page" from theroutesForNotAuthenticatedOnly
array. - If you navigate to
/login
, after a delay of 3 seconds, the login process will be simulated. It will set a test token using thesetToken
function from the authentication context, and then you will be redirected to the root page/
using thenavigate
function from thereact-router-dom
library. After the redirect, you will see the "User Home Page" from theroutesForAuthenticatedOnly
array. - If you then visit
/logout
, after a delay of 3 seconds, the logout process will be simulated. It will clear the authentication token by calling thesetToken
function without any argument, and then you will be redirected to the root page/
again. Since you are now logged out, we will see the "Home Page" from theroutesForNotAuthenticatedOnly
array.
This flow demonstrates the login and logout processes, where the user transitions between authenticated and non-authenticated states and the corresponding routes are displayed accordingly.
Top comments (36)
Please keep in mind the implications of storing secrets such as access tokens (e.g. a JWT) in
localStorage
. Browser storage such aslocalStorage
orsessionStorage
do not provide sufficient isolation against e.g. XSS attacks and the secrets kept inlocalStorage
can easily be exposed by malicious code.Further reading:
auth0.com/blog/secure-browser-stor...
snyk.io/blog/is-localstorage-safe-...
Thank you for raising this important concern. You're absolutely right that storing access tokens or any sensitive information in
localStorage
can pose security risks, especially in the context of XSS attacks. It's crucial to consider these implications and take necessary precautions.One alternative approach to mitigate such risks is to use techniques like HttpOnly cookies or secure authentication mechanisms. By employing secure storage methods and implementing best practices, we can enhance the overall security of our applications.
I appreciate your valuable input and the reminder to prioritize security in handling sensitive data.
Exellent article, well described and organized, easy to follow.
I didn't understand something on the router. It defines two "/" routes to main, but how it know which one use?
I didn't catch how the router detects when to use the public and the authorized one.
Thank you for your kind words! I'm glad you found the article helpful.
In React Router, the routes are processed in the order they are declared. This means that the first matching route will be rendered.
When a user navigates to a specific route, React Router will check the routes in the order they are defined. If a route's path matches the current URL, React Router will render the corresponding element.
When the user is authenticated (has a token), the
routesForNotAuthenticatedOnly
is excluded from the route configuration. Therefore, only the routes fromroutesForPublic
androutesForAuthenticatedOnly
will be considered. If the path matches "/", React Router will render the<ProtectedRoute />
component, which leads to the "User Home Page" component being displayed.On the other hand, when the user is not authenticated (no token), the
routesForNotAuthenticatedOnly
routes are included. In this case, if the path matches "/", React Router will render the "Home Page" component fromroutesForNotAuthenticatedOnly
, because it is declared before the routes fromroutesForAuthenticatedOnly
.The order of route declaration is significant. React Router processes the routes in the order they are defined, and the first matching route is rendered. In this scenario, since the path "/" matches the first route in
routesForNotAuthenticatedOnly
, that route's corresponding component will be rendered.So, to clarify, when the user is not authenticated, the "Home Page" component from
routesForNotAuthenticatedOnly
will be rendered when the path matches "/", as it is declared before the routes fromroutesForAuthenticatedOnly
.By conditionally including different route configurations based on the authentication status, you can control which routes are available to the user and which components are rendered for different scenarios.
That was a Mega explanation!
All clear, thanks!
Hi, very good article, but I think it is more maintainable if we use useContext with useReducer for these cases, in this way we handle possible predefined state changes and we don't let them change in any way through the code. this way we can have a useAuthStateContext and a useAuthDispatchContext to make it more maintainable and even easier to use. greetings and congratulations!
Thank you for your feedback and congratulations! I appreciate your input and agree that using
useContext
withuseReducer
can offer better maintainability for handling state changes.In my projects, I often utilize the combination of
useContext
anduseReducer
for managing authentication state. However,for the purpose of this article, I have chosen to simplify the implementation and use
useState
.Here is the updated version of the
AuthProvider
component that implements the reducer pattern:And here is the updated version of the
Logout
component that utilizes theclearToken
function from theuseAuth
hook:Again thank you for sharing your thoughts. I appreciate your support!
this guy using A.I to reply lmao
Good stuff Sanjay, thanks for sharing,
just a little clarification about file name:
authProvider.js is really authProvider.jsx
if you use .js you will get the following/similar error:
Of course you have the right file name in the source code. :-)
Good post, thank you ๐
What do you think instead of using Context API, using Effector or Zustand?
What if backend sends you an HTTP cookie (JWT), that JWT includes time of creation and time expiration. This is jus first part of the full token, with the second part stored on the server. You only need to check expiration time of the token.
Thank you for your kind words! When it comes to alternative state management solutions, libraries like
Effector
orZustand
can offer different approaches compared to the Context API. While I haven't personally worked with Effector and Zustand, I do plan on exploring them in the future. However, I can share thatRedux Toolkit
is a widely adopted and powerful state management solution that I intend to cover in an upcoming article.Regarding JWT tokens with time of creation and expiration, it is a common practice in authentication. If the backend sends the JWT as an HTTP cookie, you can extract relevant information such as the expiration time from the token and store it on the client-side. By checking the expiration time, you can determine the token's validity. If the token has expired, you may need to handle token renewal or reauthentication based on your application's requirements.
Hi,
Why do we need to create ? Why not simply do the following:
const router = createBrowserRouter([
...routesForPublic,
...(!token ? routesForNotAuthenticatedOnly : []),
...(token ? routesForAuthenticatedOnly : [] ),
]);
We check the token here in createBrowerRouter itself. This is easier and much more maintainable.
For the
routesForNotAuthenticatedOnly
, the logic is as follows:routesForNotAuthenticatedOnly
array will be included in the routes configuration.[]
will be included instead, effectively excluding theroutesForNotAuthenticatedOnly
from the routes configuration.(!token ? routesForNotAuthenticatedOnly : [])
.For the
routesForAuthenticatedOnly
, the logic is as follows:/
,/profile
, etc.), theProtectedRoute
component is responsible for checking if the user is authenticated./login
route using theNavigate
component fromreact-router-dom
.ProtectedRoute
component, where it checks if the user is authenticated and handles the redirection accordingly.Overall, these mechanisms ensure that the appropriate routes are accessible based on the user's authentication status. If the user is not authenticated, they can access the
routesForNotAuthenticatedOnly
, and if they are authenticated, they can access theroutesForAuthenticatedOnly
with the added protection of theProtectedRoute
component redirecting them to the login page if needed.Great Article.
Newly learned this function.
createBrowserRouter
Great!
It described in good order and refined explanations.
Great Article
Thanks for sharing what a great stuff easy to follow.