Handling JWT Access and Refresh Token using Axios in React App

Handling JWT Access and Refresh Token using Axios in React App

Hey Readers, How have you been? I hope you are safe and sound. Well, recently I have been working as a frontend dev for a startup and have to implement an Authentication system in their frontend.

Well if you already don't know it there are famously two ways people handle authentication in their products nowadays.

  1. JWT tokens

  2. OAuth

Well, you can learn about them on the internet there are 100s of blogs and videos for them.

Now let me explain to you guys how to use the first method in React.

JWT Token

JSON Web Tokens also commonly known as JWT are used to authenticate a client to the server. It can be used for login or other things.

JSON Web Tokens are an open, industry-standard RFC 7519 method for representing claims securely between two parties.

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

above is an example JWT web token it can be decoded in the below format

Header

{
  "alg": "HS256",
  "typ": "JWT"
}

Payload

{
  "sub": "1234567890",
  "name": "John Doe",
  "iat": 1516239022
}

Verify Signature

HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),

your-256-bit-secret

)

JWT Auth Token and JWT Refresh Token

A JWT architecture usually has two parts one is authToken and others are refreshToken. Where authToken is responsible for authenticating the user and refreshToken is responsible for getting the new authToken from the backend without asking for username and password from the user.
Yes! authTokens expires. Well for security purposes we set up our authToken in such as way that it expires in a while and uses the refresh token to fetch the authToken back.

Understanding the flow of JWT.

  1. User Logins: User logins with username (email) and password which then goes back to the server to create a new JWT token. A simple JWT token contains JWT authToken and JWT refreshToken both tokens have an expiry generally and refreshToken should always have a greater expiry date than authToken.

  2. Token Received: Once the JWT token is received by the front end they can save that JWT token into local storage or to an in-memory store like Redux. Depends on preferences but there are some standard ways to do that.

  3. Making Calls: Once the JWT token is saved, all calls which use authentication/authorisation ( Yes mate both are different concepts) uses this JWT token to successfully validate the request.

  4. Token expiry: Once the JWT token is expired which as we already know going to happen the JWT refresh token is used to authenticate the API call and used to fetch the new JWT tokens. (Note: Refresh token can only authenticate the API route which is used to get the new tokens)

  5. Using the new Auth Tokens: Once you get the new JWT tokens you can use the authTokens to make the API calls to the server.

  6. Repeat the Process: Keep repeating the process to get the new authTokens and making the API call.

Handling JWT Token in React.

Well, you might be working on just the front end and you might have an API endpoint to get the JWT authToken and JWT refreshToken.

let's create a new component to get the auth token. We will call it login.jsx and write the login-related logic here.

In this file, we will have the signup method which takes username and password as payload and gets the authToken and RefreshToken

Below is the code for the login component which is responsible for handling the login functionality

import React, { useState } from 'react';
import axios from 'axios';

const Login = () => {
  const [credentials, setCredentials] = useState({
    email: '',
    password: ''
  });

  const handleChange = (e) => {
    setCredentials({
      ...credentials,
      [e.target.name]: e.target.value
    });
  };

  const handleSubmit = async (e) => {
    e.preventDefault();

    try {
      const response = await axios.post('/api/login', credentials);
      const { token, refreshToken } = response.data;

      // Store the tokens in localStorage or secure cookie for later use
      localStorage.setItem('token', token);
      localStorage.setItem('refreshToken', refreshToken);

      // Redirect or perform other actions upon successful login
    } catch (error) {
      // Handle login error
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="email"
        name="email"
        value={credentials.email}
        onChange={handleChange}
      />
      <input
        type="password"
        name="password"
        value={credentials.password}
        onChange={handleChange}
      />
      <button type="submit">Login</button>
    </form>
  );
};

export default Login;

Let me explain what is happening in the above code.

  1. First, we are asking users to get the username and password in a form component. We use handleChange function to handle the changes to form.

  2. OnSubmit we are making an API call to the backend on /api/login endpoint, which is responsible for handling the authentication and login of the user.

  3. On successful login, we will be having the JWT authToken and JWTrefreshToken which we set in the local store for future use. (Note: Saving JWT tokens to the local storage is not secure but for simplicity purposes, we will use this method.)

Setting Up Axios Interceptor for the JWT tokens.

In this step, we set up Axios Interceptor so that in every API call Axios automatically append the header with the bearer ${authToken}, which is used to authenticate and authorize the API calls to the backend.

import axios from 'axios';

const api = axios.create({
  baseURL: '/api',
});

// Add a request interceptor
api.interceptors.request.use(
  (config) => {
    const token = localStorage.getItem('token');
    if (token) {
      config.headers.Authorization = `Bearer ${token}`;
    }
    return config;
  },
  (error) => Promise.reject(error)
);

export default api

The above code solves the problem of authenticating each request but what about the JWT token expiration? What will we do when the JWT authToken expires? Now we can ask users to log in again but that is not a good UX, a better way is to automatically fetch the new JWT authToken if the JWT refreshToken exists.

To do that we will again use Axios interceptor but not at request but at response.

// Add a response interceptor
api.interceptors.response.use(
  (response) => response,
  async (error) => {
    const originalRequest = error.config;

    // If the error status is 401 and there is no originalRequest._retry flag,
    // it means the token has expired and we need to refresh it
    if (error.response.status === 401 && !originalRequest._retry) {
      originalRequest._retry = true;

      try {
        const refreshToken = localStorage.getItem('refreshToken');
        const response = await axios.post('/api/refresh-token', { refreshToken });
        const { token } = response.data;

        localStorage.setItem('token', token);

        // Retry the original request with the new token
        originalRequest.headers.Authorization = `Bearer ${token}`;
        return axios(originalRequest);
      } catch (error) {
        // Handle refresh token error or redirect to login
      }
    }

    return Promise.reject(error);
  }
);

So let me explain the code above so that we understand why we need interceptors and how the above code is working.

On each response error with the status code 401 which is the status code for Unauthorized and no retryFlag means that the JWT token has expired. Hence we need to fetch the new token using the JWTrefreshToken and then we set the newly generated JWT authToken and send the request again for the failed request.
In case of any JWT refreshToken error we can redirect to log in so that we can fetch new JWT authToken and JWTrefreshToken.

Using the above solution in our Application.

Now, you can use the api instance from the api.js files in your components to make authenticated requests. Here's an example of usage:

import React, { useEffect, useState } from 'react';
import api from './api';

const Profile = () => {
  const [user, setUser] = useState(null);

  useEffect(() => {
    const fetchProfile = async () => {
      try {
        const response = await api.get('/profile');
        setUser(response.data);
      } catch (error) {
        // Handle error or redirect to login
      }
    };

    fetchProfile();
  }, []);

  if (!user) {
    return <div>Loading...</div>;
  }

  return (
    <div>
      <h2>Welcome, {user.name}!</h2>
      <p>Email: {user.email}</p>
      {/* Render other user details */}
    </div>
  );
};

export default Profile;

TL;DR:

To implement authentication in a React application, one popular approach is to use JSON Web Tokens (JWT). Here's a summary of the steps involved:

  1. Create a Login component that handles the login form and makes an API call to authenticate the user. Store the received JWT authToken and JWT refreshToken in localStorage.

  2. Set up an Axios instance with interceptors to automatically include the JWT authToken in the headers of authenticated API requests.

  3. Add a response interceptor to handle JWT token expiration. If a request receives a 401 error (Unauthorized) and there is a JWT refreshToken available, use it to fetch a new JWT authToken and update it in localStorage. Then retry the original request with the new authToken.

  4. Use the configured Axios instance in your components to make authenticated API calls.

Remember to handle errors, redirect to login on refreshToken failure, and consider security aspects when storing tokens.

By implementing JWT authentication with refresh tokens and using Axios interceptors, you can create a secure and efficient authentication system in your React application.

Did you find this article valuable?

Support Ashish maurya by becoming a sponsor. Any amount is appreciated!