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:
- It takes in three parameters:
args
(request arguments),api
(Redux Toolkit Query API object), andextraOptions
(additional options for the request). - Initially, it calls
baseQueryWithAuth
with the provided arguments and awaits the result. - If the result contains an error with a status code of 401, it indicates that the access token has expired or is invalid.
- 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. - 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.
- Finally, it retries the initial query by calling
baseQueryWithAuth
again with the original arguments and awaits the result. - 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.
4 comments
Excellent, thank you !
Thank you for your kind words! Happy coding :).
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((
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.