Token Validation in React: Explained with Code

in blurt •  2 years ago 

Introduction:

Many online applications must include token validation. Token validation is frequently carried out in React apps before providing access to particular routes or components. In this blog article, we'll look at a piece of code that illustrates how to use React Router, Redux, and JWT tokens to enable token validation in a React application.

Code Explanation:

The provided code demonstrates a higher-order component (HOC) called WithTokenValidation. Let's break down the code and understand its functionality step by step:


src

Import Statements:

The code begins with importing necessary modules and dependencies such as React, useEffect, useState, react-router-dom, Redux-related functions, and utility libraries like js-cookie and jwt-decode.

Component Declaration:

The WithTokenValidation component is declared as a functional component that accepts a single prop called children. This component will wrap the desired components or routes that require token validation.

State and Hooks Initialization:

Inside the WithTokenValidation component, several state variables and hooks are initialized using the useState and useEffect hooks. These hooks allow performing side effects and managing component state.

Token Validation Queries:

The code utilizes two custom Redux toolkit queries: useCheckTokenValidityQuery and useGetAccessTokenQuery. These queries are responsible for checking the validity of the token and retrieving the access token, respectively. The useNavigate hook from react-router-dom is also imported to enable programmatic navigation.

Token Validation Logic:

The first useEffect hook is triggered when the token validity query (useCheckTokenValidityQuery) completes. If the query is successful, the token status is set to "valid," and the token information is stored in the Redux store using the setTokenInfo action.

Error Handling:

If the token validity query encounters an error, the code checks the error status. If it's a server error (status 500), the user is navigated to a custom error page ("/page500"). Otherwise, the code proceeds to call the access token query (useGetAccessTokenQuery).


src

Access Token Handling:

The second useEffect hook is triggered when the access token query completes. On success, the access token is decoded using jwt_decode, and the token information is stored in the Redux store. Additionally, the access token and user-related information are stored in cookies using the Cookies utility. Finally, local storage is cleared, indicating a successful token validation.

More Error Handling:

If the access token query encounters an error, the token status is set to "invalid." If the error message indicates that the token is not valid, the setTokenInfo action is dispatched to store the error information. The removeCookies action is also dispatched to remove any stored cookies related to the authentication. Server errors (status 500) trigger navigation to the custom error page ("/page500").

Navigation and Token Status:

Two additional useEffect hooks are used to handle navigation and token status. The first one checks the token status and triggers navigation to a custom unauthorized page ("/page401") if the token is invalid and an error has occurred. The second hook sets the skip flags for the token validity query based on the presence of the access token in cookies.

Final Token Status Check:

A setTimeout function is used to determine the token status if it's still unknown after a certain delay. If the access token is found in cookies, the skip flag for the token validity query is set to false; otherwise, the token status is set to "invalid."

Rendering:

The component's render logic handles three scenarios:

If the token status is null, a loading spinner or placeholder is displayed.
If the token status is "valid," the wrapped children components or routes are rendered.
If the token status is "error," you can implement specific error handling logic here.

Certainly! Let's break down the code part by part and explain each section:

Import Statements:

import React, { useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
import BackDrop from "../shared/Backdrop/Backdrop";
import {
  useCheckTokenValidityQuery,
  useGetAccessTokenQuery,
} from "../../Redux/Slices/refreshTokenSlice";
import { useDispatch } from "react-redux";
import {
  removeCookies,
  setLoadTokenVerify,
  setTokenInfo,
  setUserEmail,
} from "../../Redux/Slices/authSlice";
import Cookies from "js-cookie";
import jwt_decode from "jwt-decode";

In this section, the necessary dependencies, modules, and libraries are imported. React, useEffect, useState, and useNavigate are imported from the "react" and "react-router-dom" libraries. The "BackDrop" component is imported from the specified file path. useCheckTokenValidityQuery and useGetAccessTokenQuery are imported from the Redux slice file "refreshTokenSlice", allowing access to the custom Redux toolkit queries. useDispatch is imported from "react-redux" for dispatching actions. The action creators and utilities from the Redux slice files "authSlice" and "js-cookie" are imported. The "jwt-decode" library is imported to decode JWT tokens.

Component Declaration:

export const WithTokenValidation = ({ children }) => {
  const [tokenStatus, setTokenStatus] = useState(null);
  const navigate = useNavigate();
  const [skipToCallAccessTokenQuery, setSkipToCallAccessTokenQuery] =
    useState(true);
  const [skipToCallGetTokenInfoQuery, setSkipToCallGetTokenInfoQuery] =
    useState(true);
  const dispatch = useDispatch();
  const {
    data: tokenValidityInfo,
    isSuccess: isSuccessTokenInfoAPI,
    error: errorTokenInfoAPI,
    isError: isErrorTokenInfoAPI,
  } = useCheckTokenValidityQuery({}, { skip: skipToCallGetTokenInfoQuery });
  
  // Rest of the code...
}

The WithTokenValidation component is declared as a functional component that receives the children prop. Inside the component, various state variables are initialized using the useState hook. The tokenStatus state variable keeps track of the token's validation status. The navigate constant is assigned the useNavigate hook from react-router-dom, which enables programmatic navigation. Two more state variables, skipToCallAccessTokenQuery and skipToCallGetTokenInfoQuery, control whether the access token and token validity queries should be called or skipped. The dispatch function is used to dispatch actions from the Redux store. The useCheckTokenValidityQuery hook is used to make the token validity query, and the resulting data, success status, error, and error status are destructured from the hook's return value.

Token Validation Logic:

useEffect(() => {
  try {
    if (isSuccessTokenInfoAPI) {
      setTokenStatus("valid");
      dispatch(setTokenInfo(tokenValidityInfo));
    }
    if (isErrorTokenInfoAPI) {
      if (errorTokenInfoAPI?.status === 500) {
        navigate("/page500");
      } else {
        setSkipToCallAccessTokenQuery(false);
      }
    }
  } catch (error) {
    navigate("/page500");
  }
  // eslint-disable-next-line react-hooks/exhaustive-deps
}, [isErrorTokenInfoAPI, isSuccessTokenInfoAPI, tokenValidityInfo]);

This useEffect hook is responsible for handling the token validity query's success and error cases. If the token validity query is successful (isSuccessTokenInfoAPI is true), the tokenStatus is set to "valid," and the setTokenInfo action is dispatched to store the token validity information in the Redux store. If there's an error in the token validity query (isErrorTokenInfoAPI is true), it checks if the error status is 500 (server error). If it is, the user is navigated to the custom error page ("/page500"). Otherwise, it means the token is invalid or expired, and the skipToCallAccessTokenQuery flag is set to false, indicating that the access token query should be called.

Access Token Handling:

useEffect(() => {
  try {
    if (isSuccessAccessTokenAPI) {
      const decodeAccessToken = jwt_decode(accessToken?.access_token);
      dispatch(setTokenInfo(decodeAccessToken));
      setTokenStatus("valid");
      const decoded = jwt_decode(accessToken?.access_token);

      Cookies.set(
        "jwtTokenCredentialsAccessToken",
        accessToken?.access_token
      );
      Cookies.set("userId", decoded.sub);
      Cookies.set("season", "");
      dispatch(setLoadTokenVerify(true));
      dispatch(setUserEmail({ userId: decoded.sub, authenticated: true }));
      localStorage.clear();
    }
    if (isErrorAccessTokenAPI) {
      setTokenStatus("invalid");
      if (errorAccessTokenAPI?.data?.detail === "Token is not valid") {
        dispatch(setTokenInfo(errorAccessTokenAPI?.data));
        dispatch(removeCookies());
      }
      if (errorAccessTokenAPI?.status === 500) {
        navigate("/page500");
      }
    }
  } catch (error) {
    navigate("/page500");
  }
  // eslint-disable-next-line react-hooks/exhaustive-deps
}, [isErrorAccessTokenAPI, isSuccessAccessTokenAPI, accessToken]);


src
This useEffect hook handles the success and error cases of the access token query. If the query is successful (isSuccessAccessTokenAPI is true), the access token is decoded using jwt_decode, and the decoded token information is stored in the Redux store using the setTokenInfo action. The tokenStatus is set to "valid" to indicate a successful token validation. Additionally, the access token, user ID, and other relevant information are stored in cookies using the Cookies utility. The setLoadTokenVerify action is dispatched to indicate that token verification is complete. The setUserEmail action is dispatched to set the user's email and authentication status. Local storage is cleared to remove any previously stored data.

If there's an error in the access token query (isErrorAccessTokenAPI is true), the tokenStatus is set to "invalid." If the error message indicates that the token is not valid, the setTokenInfo action is dispatched to store the error information in the Redux store. The removeCookies action is dispatched to remove any stored cookies related to authentication. If the error status is 500 (server error), the user is navigated to the custom error page ("/page500").

Authors get paid when people like you upvote their post.
If you enjoyed what you read here, create your account today and start earning FREE BLURT!
Sort Order:  
  ·  2 years ago  ·  

Congratulations, your post has been upvoted by @dsc-r2cornell, which is the curating account for @R2cornell's Discord Community.

Manually curated by @jasonmunapasee

r2cornell_curation_banner.png