darkylmnx
darkylmnx

Reputation: 2091

How to handle JWT refresh token on client-side with multiple requests / API calls in parallel?

I'm having an issue where I do not find a security-first and maintenable answer anywhere.

Imagine a dashboard doing multiple queries at the same time, how do you handle refresh_tokens in a clean and stadard way?

The stack is (even if the stack doesn't matter here):

Backend - Laravel with a JWT token authentification Frontend - Vue JS with axios for the API calls

Endpoints:

JWT refresh workflow

// ... just some pseudo code
userDidAction() {
  axios.get('/statistics').then(res => handleThis(res.data));
  axios.get('/other-statistics').then(res => handleThat(res.data));
  axios.get('/event-more-statistics').then(res => handleThisAgain(res.data));
  axios.get('/final-statistics').then(res => handleThatAgain(res.data));
}
// ...

This is a very common scenario on SPAs and SaaS apps. Having multiple asynchronous API calls is not an edge case.

What are my options here ?

My current idea is to make the access_token last 3 days and the refresh_token last a month with the following workflow :

This makes the refresh_token travel less on the network and makes parallel fails impossible since we change tokens only when the frontend loads initially and therefore, tokens would live for at least 12h before failing.

Despite this solution working, I'm looking for a more secure / standard way, any clues?

Upvotes: 8

Views: 6856

Answers (1)

hamid niakan
hamid niakan

Reputation: 2871

So here is the situation I had in an application and the way I solved it:

application setup

  • Nuxt application
  • Uses axios for API calls
  • Uses Vuex for state management
  • Uses JWT token which expires every 15 minutes so whenever this happens there should be an API call to refresh the token and repeat the failed request

token

I saved the token data in a session storage and update it with refresh token API response each time

Problem

I had three get request in one page, and I wanted this behavior that when token expires ONLY one of them get to call the refresh Token API and the others have to wait for the response, when the refresh token promise is resolved all three of them should repeat the failed request with updated token data

Solution with axios interceptors and vuex

So here is the vuex setup:

// here is the state to check if there is a refresh token request proccessing or not  
export const state = () => ({
  isRefreshing: false,
});

// mutation to update the state
export const mutations = {
  SET_IS_REFRESHING(state, isRefreshing) {
    state.isRefreshing = isRefreshing;
  },
};

// action to call the mutation with a false or true payload
export const actions = {
  setIsRefreshing({ commit }, isRefreshing) {
    commit('SET_IS_REFRESHING', isRefreshing);
  },
};

and here is the axios setup:

import { url } from '@/utils/generals';

// adding axios instance as a plugin to nuxt app (nothing to concern about!)
export default function ({ $axios, store, redirect }, inject) {

  // creating axios instance
  const api = $axios.create({
    baseURL: url,
  });

  // setting the authorization header from the data that is saved in session storage with axios request interceptor
  api.onRequest((req) => {
    if (sessionStorage.getItem('user'))
      req.headers.authorization = `bearer ${
        JSON.parse(sessionStorage.getItem('user')).accessToken
      }`;
  });

  // using axios response interceptor to handle the 401 error
  api.onResponseError((err) => {
    // function that redirects the user to the login page if the refresh token request fails
    const redirectToLogin = function () {
      // some code here
    };

    if (err.response.status === 401) {
      // failed API call config
      const config = err.config;
      
      // checks the store state, if there isn't any refresh token proccessing attempts to get new token and retry the failed request
      if (!store.state.refreshToken.isRefreshing) {
        return new Promise((resolve, reject) => {
          // updates the state in store so other failed API with 401 error doesnt get to call the refresh token request
          store.dispatch('refreshToken/setIsRefreshing', true);
          let refreshToken = JSON.parse(sessionStorage.getItem('user'))
            .refreshToken;

          // refresh token request
          api
            .post('token/refreshToken', {
              refreshToken,
            })
            .then((res) => {
              if (res.data.success) {
                // update the session storage with new token data
                sessionStorage.setItem(
                  'user',
                  JSON.stringify(res.data.customResult)
                );
                // retry the failed request 
                resolve(api(config));
              } else {
                // rediredt the user to login if refresh token fails
                redirectToLogin();
              }
            })
            .catch(() => {
                // rediredt the user to login if refresh token fails
              redirectToLogin();
            })
            .finally(() => {
              // updates the store state to indicate the there is no current refresh token request and/or the refresh token request is done and there is updated data in session storage
              store.dispatch('refreshToken/setIsRefreshing', false);
            });
        });
      } else {
        // if there is a current refresh token request, it waits for that to finish and use the updated token data to retry the API call so there will be no Additional refresh token request
        return new Promise((resolve, reject) => {
          // in a 100ms time interval checks the store state
          const intervalId = setInterval(() => {
            // if the state indicates that there is no refresh token request anymore, it clears the time interval and retries the failed API call with updated token data
            if (!store.state.refreshToken.isRefreshing) {
              clearInterval(intervalId);
              resolve(api(config));
            }
          }, 100);
        });
      }
    }
  });

  // injects the axios instance to nuxt context object (nothing to concern about!)
  inject('api', api);
}

and here is the situation as shown in network tab:

enter image description here

as you can see here there are three failed request with 401 error, then there is one refreshToken request, after that all failed requests get called again with updated token data

Upvotes: 6

Related Questions