How to handle JWT Token Refresh cycle with React and RTK Query: Refetching the failed queries automaticallly

by admin
rtk-query

RTK Query is awesome, if you don’t know the library. You can find it here.

We use it in pretty much every project. We also use token authentication in many projects. We’ve created a base RTK query service that handles automatically getting a new accessToken whenever needed. It also sets the correct headers and makes sure that it uses our accessToken as an Authentication header.

The server looks like this:

import { BaseQueryFn, FetchArgs, fetchBaseQuery, FetchBaseQueryError } from '@reduxjs/toolkit/query/react'
import { loggedOut } from '../slices/authStore'

let isRefreshing = false // Flag to indicate if token refresh is in progress
let refreshPromise: any = null // To hold the refresh token promise

const baseQueryWithAuth = fetchBaseQuery({
  baseUrl: process.env.NEXT_PUBLIC_API_URL,
  prepareHeaders: async headers => {
    const accessToken = localStorage.getItem('accessToken')

    if (accessToken) {
      headers.set('Authorization', `Bearer ${accessToken}`)
    }

    headers.set('Accept', 'application/json')
    headers.set('Cache-Control', 'no-cache')
    headers.set('Pragma', 'no-cache')
    headers.set('Expires', '0')

    return headers
  }
})

const baseQueryWithReauth: BaseQueryFn<string | FetchArgs, unknown, FetchBaseQueryError> = async (
  args,
  api,
  extraOptions
) => {
  let result = await baseQueryWithAuth(args, api, extraOptions)
   if (result.error && result.error.status === 401) {
    if (!isRefreshing) {
      isRefreshing = true // Indicate refresh is in progress
      refreshPromise = Promise.resolve(
        baseQueryWithAuth(
          {
            url: `/v1/common/authenticate/RefreshToken`,
            method: 'POST',
            body: {
              token: localStorage.getItem('refreshToken')
            }
          },
          api,
          extraOptions
        )
      )
        .then(refreshResult => {
          if (refreshResult.data) {
            const refreshTokenResult = refreshResult.data as any

            // Store the new tokens
            localStorage.setItem('accessToken', refreshTokenResult.data.accessToken)
            localStorage.setItem('refreshToken', refreshTokenResult.data.refreshToken)
            isRefreshing = false // Reset the flag

            return refreshTokenResult
          } else {
            // Handle refresh error
            isRefreshing = false // Ensure flag is reset for future requests
            if (refreshResult?.error?.status === 500) {
              api.dispatch(loggedOut())
              location.reload()
            }

            return null
          }
        })
        .catch((error: any) => {
          console.error('Error refreshing token', error)
          isRefreshing = false // Reset flag on error
        })
    }

    await refreshPromise // Wait for the refresh token request to complete
    result = await baseQueryWithAuth(args, api, extraOptions) // Retry the initial query
  }

  return result
}
export const {} = baseQueryWithReauth

You just have to replace the NEXT_PUBLIC_API_URL with your own API url.

The provided code snippet showcases two functions: baseQueryWithAuth and baseQueryWithReauth. Let’s delve into each function’s purpose and functionality.

baseQueryWithAuth:
This function is responsible for setting up the base configuration for making authenticated API requests using Redux Toolkit Query. It utilizes the fetchBaseQuery function provided by @reduxjs/toolkit/query/react. Here’s what it does:

  • It configures the base URL for API requests using the value stored in process.env.NEXT_PUBLIC_API_URL.
  • It defines a prepareHeaders function to modify the request headers before each API call. This function checks if an access token is available in the local storage. If an access token exists, it adds an “Authorization” header with the token value prefixed by “Bearer”. Additionally, it sets headers such as “Accept”, “Cache-Control”, “Pragma”, and “Expires” for desired request behavior.

The function returns the modified headers.

baseQueryWithReauth
This function extends the functionality of baseQueryWithAuth and handles token refresh if an API request returns a 401 (Unauthorized) status code. Here’s what it does:

  1. It takes in three parameters: args (request arguments), api (Redux Toolkit Query API object), and extraOptions (additional options for the request).
  2. Initially, it calls baseQueryWithAuth with the provided arguments and awaits the result.
  3. If the result contains an error with a status code of 401, it indicates that the access token has expired or is invalid.
  4. In such a scenario, the function attempts to refresh the token by sending a request to the /Auth/RefreshToken endpoint with the refresh token stored in the local storage.
  5. If the refresh token request succeeds and returns a new access token, it updates the access token and refresh token values in the local storage.
  6. Finally, it retries the initial query by calling baseQueryWithAuth again with the original arguments and awaits the result.
  7. If the refresh token request fails or does not return a new token, it dispatches a Redux action (loggedOut) to log out the user. The logged out method just clears the accessToken, refreshToken and some other items from the localstorage.

Conclusion: The provided code demonstrates an effective approach to handle authentication and token refresh in Redux Toolkit Query. By configuring the baseQueryWithAuth function, you can ensure that all API requests include the necessary authorization headers. Additionally, the baseQueryWithReauth function handles token refresh automatically when an unauthorized error occurs, allowing for seamless user experiences in authenticated applications.

Related Posts

4 comments

Yann August 14, 2023 - 12:28 pm

Excellent, thank you !

Reply
admin August 16, 2023 - 8:48 am

Thank you for your kind words! Happy coding :).

Reply
Pavel October 30, 2023 - 9:21 am

Here is a problem, if the page needs 2 or more requests, token will request several times. But i dont know how to solve it((

Reply
admin February 24, 2024 - 8:25 am

Hi Pavel. I am so sorry for the very late reply.. I hope you got it all sorted by now. But if not, I have updated the post. We can make sure that te refreshtoken flow is only executed once by wrapping it inside a promise and holding the status in a global variable at the top of the file.

Reply

Leave a Comment