Configure Access and Refresh JWTs in React App

Configure Access and Refresh JWTs in React App

A step wise guide to integrate the access and refresh tokens into your app’s authentication logic

JSON Web Token is a proposed Internet standard for producing data (tokens) with optional signatures and/or encryption, with the payload including JSON that asserts a set of claims. — Wikipedia

I assume you know about axios, create-react-app, and little about the JWT tokens’ concept; and have already setup the login functionality of your app and now want to integrate the JWT tokens in your app.

In this tutorial, I’m going to show how to setup the refresh and access tokens for a react app. Below are the steps that a react app’s flow requires for the implementation of the authentication using Refresh and Access JWTs, and this is how most of the apps’ authentication work:

  1. The user enters login credentials, and after verifying, the backend generates and sends back refresh and access tokens.
  2. Whenever the user sends an http request after logging in, the access token will be attached with that request’s authorization headers.
  3. And before sending the http request, the access token’s expiry time will be checked if it is still valid token. If it is valid, then the request will be forwarded. Else:
  4. If the access token is not valid, then the refresh token will be used to refresh the access token. But before proceeding with this step, the refresh token will also be checked if it is valid or not. If the refresh token is not valid, then the user will be logged out, or else the former case will be executed (that is to refresh the access token by sending the refresh token as body).
  5. If the user presses log out button, then in that case both the tokens will be deleted and the user will be taken back to log in screen.

The above steps will be referenced when coding our logic. There are some corner cases which we will deal with in the coding section.

Npm packages to be installed (if you don’t have already):

We need axios because we use Axios interceptors to intercept each http communication. A quick note: if you have implemented fetch() method instead of axios() for the http communications in your app, it will not intercept the http communications in the Axios interceptors. The Axios interceptor functions intercept the http requests that are sent by only the Axios method. It is an internal axios mechanism. Between, my own app was based on the fetch method; so I had to change my app from fetch to axios in order to use the axios interceptors.

jwt-decode is used to decode the tokens and get their expiry time to work with it. And js-cookie is used for storing the access token; whereas the refresh token is stored inside local storage. And the moment library is used for the date and time formatting.


Alright enough talk, now let’s get into the code part.

Step 1: When the user is logging into the app, the login credentials are sent, and in response, the access and refresh tokens are received. The refresh token is stored inside local storage, while the access token is stored inside the js-cookie. Like so:

const { access_token, refresh_token } = res.data;
Cookies.set("access", access_token);
localStorage.setItem("refresh_token", refresh_token);

Steps 3 and 4: (Don't get confused by steps' numbering, step 2 is coming after these steps. It would be better to look at step 2 after these steps for making more sense.) We need a separate method that will be executed inside the axios interceptors to validate the token before sending any http request, except when the user is logging in, or when the refresh token is sent for acquiring the access token. Here is that TokenValidate method.

import axios from "axios";
import Cookies from "js-cookie";
import jwt_decode from "jwt-decode";
import moment from "moment";
import { history } from "../index";

const TokenValidate = async () => {
let access_token = Cookies.get("access");
 let refresh_token = localStorage.getItem("refresh_token");
if (!refresh_token) return history.push("/logout");

 let accessTokenExpireTime;

 try {  
   //extracting the token's expiry time with jwt_decode
   accessTokenExpireTime = jwt_decode(access_token).exp;
 } catch (error) {
   return history.push("/logout");
 }
if (moment.unix(accessTokenExpireTime) - moment(Date.now()) < 10000) {
   //generating new accessToken
   let refreshTokenExpireTime;

   try {
     refreshTokenExpireTime = jwt_decode(refresh_token).exp;
   } catch (error) {
     return history.push("/logout");
   }
if (moment.unix(refreshTokenExpireTime) - moment(Date.now()) > 10000) {
     return new Promise((resolve, reject) => {
       axios
        .post("/refreshToken", { refresh_token: refresh_token })
        .then((res) => {
          if (!res?.data?.access_token) {
          //the execution will never reach in this block, and if it did, it could be some backend issue.
            console.log("refresh token is gone");
            return history.push("/logout");
          } else {
            //refreshed the access token
            const { access_token } = res?.data;
            Cookies.set("access", access_token);
            resolve(access_token);
          }
        });
     });
   } else {
     //refreshToken expired
     Cookies.remove("access");
     localStorage.removeItem("refresh_token");
     history.push("/logout");
     alert("Your session has expired, please login again.");
   }
   return access_token;
 }
 return access_token;
};
export default TokenValidate;

You can keep this file in the api folder. One thing to note here that I have used the history hook in TokenValidate.js, which is not a react component. So what I have done is that I imported the history hook from src/index.js file. And in the index.js file, I did some modifications for the history hook to work correctly. I used the "Router" instead of "BrowserRouter" to work with routes in the app. One more thing that, earlier I had wrapped the BrowserRouter around the app routes inside app.js file; but now for injecting history in Router, I also changed that and removed the BrowserRouter from app.js file and wrapped the App component inside the index.js file with Router. Here are these modifications in the index.js file:

import { Router } from "react-router-dom";
import { createBrowserHistory } from "history";
export const history = createBrowserHistory();
ReactDOM.render(
  <Provider store={store}>
   // injecting history in the Router as well
   <Router history={history}>
      <App />
    </Router>
  </Provider>,
  document.getElementById("root")
);

Step 2: Now for sending the access token with each http request, axios interceptors come to the play. Here are both the request and response interceptors.

import axios from "axios";
import Cookies from "js-cookie";
import TokenValidate from "./TokenValidate";
//Request Interceptor
axios.interceptors.request.use(
   async (config) => {
    if (config.url.includes("/login")) return config;
    if (config.url.includes("/refreshToken")) return config;
    }
    TokenValidate();
    config.headers["Authorization"] = "Bearer " + Cookies.get("access");
    config.headers["Content-Type"] = "application/json";
    return config;
  },
(error) => {
    return Promise.reject(error);
}
);
// Response Interceptor
axios.interceptors.response.use(
  (response) => {
    return response;
  },
  (error) => {
    const request = error.config; //this is actual request that was sent, and error is received in response to that request
    if (error.response.status === 401 && !request._retry) {
      request._retry = true;
      axios.defaults.headers.common["Authorization"] =
      "Bearer " + Cookies.get("access");
      axios.defaults.headers.common["Content-Type"] = "application/json";
      return axios(request);
    } 
    return Promise.reject(error);
  }
);

Let me explain a bit about the above interceptor functions. The request interceptor is the one that every request initially passes through. And so we have attached the access token with every http request using this interceptor. We can also add exceptions to that, for example I have added two exceptions that if the http request includes the "/login" or "/refreshToken" address, then I don't want to attach the access token with that request and I've just returned the config without attaching headers. Because in this case, "/login" url has the login credentials already attached as headers instead of access token; similarly when I want to refresh the access token, then I don't need to attach the existing access token which is presumably expired that's why the "/refreshToken" http request was made by the TokenValidate.js file; and the refresh_token was attached as body from that file. Request interceptor has an error section where if an error occurs during the sending of an http request, it gets triggered from there.

The response interceptor is triggered when something is returned from the backend; and axios lets us work with the response before it is sent to the code. Thus if an error is thrown from the backend, it comes in the error section of the response interceptor. In the error section of response interceptor, I have set a condition that checks if the error code is 401 and the request is not being sent "thrice", then send the request again with the access_token as header. This is because, in the request interceptor, if the access token was found expired, it was refreshed by the TokenValidate.js; and thus that request went forward and created a 401 error; so to send that same request again, I'm sending it with the fresh access token.

You can keep these interceptors inside the src/index.js file. OR if you have created an instance of axios in your project, then you can paste the interceptors in that axios instance file with this change:

//change the following
axios.interceptors.request.use(...);
axios.interceptors.reponse.use(...);
//into like this
axiosInstance.interceptors.request.use(...);
axiosInstance.interceptors.response.use(...);
//axiosInstance be changed with the name of your axios' instance

Step 5: And finally, clear both the tokens whenever the user is logged out, like so:

Cookies.remove("access");
localStorage.removeItem("refresh_token");
history.push("/logout");
alert("Your session has expired, please login again.");

That's all for today guys. I hope you have learned something new today. Enjoy working with JWTs and React to create secure and powerful apps.

Cheers!