DEV Community

JWT Authentication in React with react-router

Sanjay Arya on May 28, 2023

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 ...
Collapse
 
crayoncode profile image
crayoncode

Please keep in mind the implications of storing secrets such as access tokens (e.g. a JWT) in localStorage. Browser storage such as localStorage or sessionStorage do not provide sufficient isolation against e.g. XSS attacks and the secrets kept in localStorage can easily be exposed by malicious code.

Further reading:
auth0.com/blog/secure-browser-stor...
snyk.io/blog/is-localstorage-safe-...

Collapse
 
sanjayttg profile image
Sanjay Arya

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.

Collapse
 
equiman profile image
Camilo Martinez • Edited

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.

Collapse
 
sanjayttg profile image
Sanjay Arya • Edited

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 from routesForPublic and routesForAuthenticatedOnly 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 from routesForNotAuthenticatedOnly, because it is declared before the routes from routesForAuthenticatedOnly.

  // Combine and conditionally include routes based on authentication status
  const router = createBrowserRouter([
    ...routesForPublic,
    ...(!token ? routesForNotAuthenticatedOnly : []),
    ...routesForAuthenticatedOnly,
  ]);
Enter fullscreen mode Exit fullscreen mode

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 from routesForAuthenticatedOnly.

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.

Collapse
 
equiman profile image
Camilo Martinez

That was a Mega explanation!
All clear, thanks!

Collapse
 
kevinosoriocodes profile image
KevinOsorioCodes

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!

Collapse
 
sanjayttg profile image
Sanjay Arya

Thank you for your feedback and congratulations! I appreciate your input and agree that using useContext with useReducer can offer better maintainability for handling state changes.

In my projects, I often utilize the combination of useContext and useReducer 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:

import axios from "axios";
import { createContext, useContext, useMemo, useReducer } from "react";

// Create the authentication context
const AuthContext = createContext();

// Define the possible actions for the authReducer
const ACTIONS = {
  setToken: "setToken",
  clearToken: "clearToken",
};

// Reducer function to handle authentication state changes
const authReducer = (state, action) => {
  switch (action.type) {
    case ACTIONS.setToken:
      // Set the authentication token in axios headers and local storage
      axios.defaults.headers.common["Authorization"] = "Bearer " + action.payload;
      localStorage.setItem("token", action.payload);

      // Update the state with the new token
      return { ...state, token: action.payload };

    case ACTIONS.clearToken:
      // Clear the authentication token from axios headers and local storage
      delete axios.defaults.headers.common["Authorization"];
      localStorage.removeItem("token");

      // Update the state by removing the token
      return { ...state, token: null };

    // Handle other actions (if any)

    default:
      console.error(
        `You passed an action.type: ${action.type} which doesn't exist`
      );
  }
};

// Initial state for the authentication context
const initialData = {
  token: localStorage.getItem("token"),
};

// AuthProvider component to provide the authentication context to children
const AuthProvider = ({ children }) => {
  // Use reducer to manage the authentication state
  const [state, dispatch] = useReducer(authReducer, initialData);

  // Function to set the authentication token
  const setToken = (newToken) => {
    // Dispatch the setToken action to update the state
    dispatch({ type: ACTIONS.setToken, payload: newToken });
  };

  // Function to clear the authentication token
  const clearToken = () => {
    // Dispatch the clearToken action to update the state
    dispatch({ type: ACTIONS.clearToken });
  };

  // Memoized value of the authentication context
  const contextValue = useMemo(
    () => ({
      ...state,
      setToken,
      clearToken,
    }),
    [state]
  );

  // Provide the authentication context to the children components
  return (
    <AuthContext.Provider value={contextValue}>{children}</AuthContext.Provider>
  );
};

// Custom hook to easily access the authentication context
export const useAuth = () => {
  return useContext(AuthContext);
};

export default AuthProvider;
Enter fullscreen mode Exit fullscreen mode

And here is the updated version of the Logout component that utilizes the clearToken function from the useAuth hook:

import { useNavigate } from "react-router-dom";
import { useAuth } from "../provider/authProvider";

const Logout = () => {
  const { clearToken } = useAuth();
  const navigate = useNavigate();

  // Function to handle logout
  const handleLogout = () => {
    clearToken(); // Clear the authentication token
    navigate("/", { replace: true }); // Navigate to the home page ("/") with replace option set to true
  };

  // Automatically logout after 3 seconds
  setTimeout(() => {
    handleLogout(); // Invoke the logout action
  }, 3 * 1000);

  return <>Logout Page</>;
};

export default Logout;
Enter fullscreen mode Exit fullscreen mode

Again thank you for sharing your thoughts. I appreciate your support!

Collapse
 
leandroruel profile image
Leandro RR

this guy using A.I to reply lmao

Collapse
 
wadigzon profile image
WADIGZON DIAZ-WONG

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:

The esbuild loader for this file is currently set to "js" but it must be set to "jsx" to be able to parse JSX syntax. You can use "loader: { '.js': 'jsx' }" to do that.

Of course you have the right file name in the source code. :-)

Collapse
 
victor_kasap_739a7ea56b2a profile image
Victor Kasap

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.

Collapse
 
sanjayttg profile image
Sanjay Arya

Thank you for your kind words! When it comes to alternative state management solutions, libraries like Effector or Zustand 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 that Redux 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.

Collapse
 
tapansharma profile image
Tapan Sharma

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.

Collapse
 
sanjayttg profile image
Sanjay Arya

For the routesForNotAuthenticatedOnly, the logic is as follows:

  • If the user is not authenticated (token is falsy), the routesForNotAuthenticatedOnly array will be included in the routes configuration.
  • If the user is authenticated (token is truthy), an empty array [] will be included instead, effectively excluding the routesForNotAuthenticatedOnly from the routes configuration.
  • This logic is achieved using the conditional operator (!token ? routesForNotAuthenticatedOnly : []).

For the routesForAuthenticatedOnly, the logic is as follows:

  • The routes are always included in the routes configuration.
  • However, when a user visits any of the protected routes (e.g., /, /profile, etc.), the ProtectedRoute component is responsible for checking if the user is authenticated.
  • If the user is not authenticated (token is falsy), they will be redirected to the /login route using the Navigate component from react-router-dom.
  • This redirection ensures that only authenticated users can access the protected routes.
  • The logic for this redirection is implemented within the 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 the routesForAuthenticatedOnly with the added protection of the ProtectedRoute component redirecting them to the login page if needed.

Collapse
 
eageringdev profile image
Eagering Dev

Great Article.
Newly learned this function.
createBrowserRouter

Collapse
 
reacthunter0324 profile image
React Hunter

Great!
It described in good order and refined explanations.

Collapse
 
pauldumebi profile image
Paul Dumebi

Great Article

Collapse
 
mezieb profile image
Okoro chimezie bright

Thanks for sharing what a great stuff easy to follow.

Collapse
 
bemimg profile image
Bernardo Guerreiro

Wish I could send this to every programmer that had the same struggles that I did, not only did you gave a perfect explanation on handling the token but also routes.
Great job and thank you.

Collapse
 
princemuel profile image
Prince Muel

This article was amazing. I'm using the createBrowserRoutesFromElements function, but it seems I'll change that.
A question I have for you is, how would someone handle refresh tokens using this setup? Thanks

Collapse
 
vidhanshu profile image
vidhanshu borade • Edited

But if we decide whether user is authenticated or not, just on the basis of existence of token, is it okay? won't if user manually add some garbage into the localStorage via inspecting, will let him sign in?
Is there any solution for this?
one solution I think, we can verify the validity of jwt in frontend, ig?

Collapse
 
selub profile image
Sergey Lubimov

same question

Collapse
 
sanjayttg profile image
Sanjay Arya

Your concern about relying solely on the existence of a token in localStorage for user authentication is valid. While it may not directly compromise security, it can affect the user experience. Here are two strategies to address this issue:

  1. Token Validity and Expiration: Upon receiving a token, it's crucial to check its validity and expiration. If the token has expired, redirecting the user to the login page is appropriate. Additionally, implementing a timeout based on the token's expiration time can automatically log out the user, enhancing security.
  2. Backend Token Validation: When redirecting the user to the home page, we can encounter an invalid token. Although the token hasn't expired, we cannot guarantee its origin. To verify that the token was indeed issued by our backend, we can create an API endpoint for token validation. This way, after checking for token expiration, we can asynchronously validate the token's authenticity with the backend, displaying a loading page to the user during this process.
// Code for Token Validity and Expiration Strategy
import axios from 'axios';
import { createContext, useContext, useEffect, useMemo, useState } from 'react';

const AuthContext = createContext();

// Function to check if JWT is expired
const checkTokenValidity = (token) => {
  if (!token) return false; // Token doesn't exist

  const decodedToken = JSON.parse(atob(token.split('.')[1]));
  const expirationTime = decodedToken.exp * 1000; // Convert to milliseconds

  return Date.now() < expirationTime; // Check if token is not expired
};

// Function to handle expired tokens
const handleExpiredToken = () => {
  delete axios.defaults.headers.common['Authorization'];
  localStorage.removeItem('token');
  // Redirect to login page or handle expired token
};

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 hook to handle token expiration and validity checks
  useEffect(() => {
    // Check if token exists and is valid
    if (token && checkTokenValidity(token)) {
      axios.defaults.headers.common['Authorization'] = 'Bearer ' + token;
      localStorage.setItem('token', token);

      // Calculate the time until token expiration
      const expirationTime = JSON.parse(atob(token.split('.')[1])).exp * 1000; // Convert to milliseconds
      const timeUntilExpiration = expirationTime - Date.now();

      // Set a timeout to automatically log out the user when the token expires
      setTimeout(() => {
        handleExpiredToken();
      }, timeUntilExpiration);
    } else {
      // Token is invalid or expired, handle accordingly
      handleExpiredToken();
    }
  }, [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;
Enter fullscreen mode Exit fullscreen mode

Implementing these measures not only ensures a smoother user experience but also strengthens the overall security of the authentication mechanism.

Collapse
 
winglessmachine profile image
winglessmachine

wow , awesome tutorial, straight to the point! just created an account here to give feedback :D

Collapse
 
denysvuika profile image
Denys Vuika

While the article features JWT in the title (a clickbait apparently), there is no JTW in the content or github example, besides setToken("this is a test token");

Collapse
 
thawhtin_aung_d2cc6ad52b profile image
Thaw Htin Aung

If I change
const routesForNotAuthenticatedOnly = [
{
path: "/",
element:,
},
];
to that,this exception occurs "Objects are not valid as a React child (found: [object RegExp]). If you meant to render a collection of children, use an array instead."

Collapse
 
sanjayttg profile image
Sanjay Arya • Edited

You need to provide the element you want to render when a path is visited.

const routesForNotAuthenticatedOnly = [
{
path: "/",
element: 'NEED_TO_PROVIDE_COMPONENT_HERE',
},
];

Collapse
 
vehibip22 profile image
vehibip225@glalen.com

how can i handle nested route.
currently at / i have component

and inside i have structure of login page like in Login compo i divided page in two half one half has image and in second half based on route i render either login or sign up . so fo that i have two route set up . at / i have and at at /sign-in i have

with your auth set up this is not working.

Collapse
 
aguy profile image
Guy Bon

Nice article although, I face a problem.

I follow your steps, and when I login, I am navigated to the path "/" which is what I want, *but * the component/element is not showing up (it's a blank page). Only if I reload the page, it will come up. Why may that be?

Collapse
 
abhidadhaniya23 profile image
Abhi Dadhaniya

Thank you so much man for this beautiful article with the simplest explanation.
Very helpful.

Collapse
 
vinaydevs profile image
Vinay Dev S

Thanks Alot❤️ , Article is super simple to understand even I am beginner.

Collapse
 
avraham_hamu profile image
Avraham Hamu

very good template thanks a lot

Collapse
 
rasel profile image
Rasel

Exellent article

Collapse
 
chideracode profile image
Chidera Humphrey

Very nice and detailed article. Simplified the concept for me.

Collapse
 
lawal49 profile image
Lawal49

Great work here

Collapse
 
rohan_almeida_3fe68a665c0 profile image
Rohan Almeida

Great post!

Collapse
 
ranjititio profile image
RanjitItio

Greate Article