Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Refreshing access token with multiple requests

Im struggling with getting axios interceptors to work.

When my token expires, i need it to refresh the access token and retry the original request once the token is refreshed. I have this part working.

The problem is if i have concurrent api calls it will only retry the first request when the token was first invalid.

Here is my interceptor code:

    export default function execute() {
  let isRefreshing = false

  // Request
  axios.interceptors.request.use(
    config => {
      var token = Storage.getAccessToken() //localStorage.getItem("token");
      if (token) {
        console.log('Bearer ' + token)
        config.headers['Authorization'] = 'Bearer ' + token
      }
      return config
    },
    error => {
      return Promise.reject(error)
    }
  )

  // Response
  axios.interceptors.response.use(
    response => {
      return response
    },
    error => {
      const originalRequest = error.config
      // token expired
      if (error.response.status === 401) {
        console.log('401 Error need to reresh')

        originalRequest._retry = true

        let tokenModel = {
          accessToken: Storage.getAccessToken(),
          client: 'Web',
          refreshToken: Storage.getRefreshToken()
        }
        //Storage.destroyTokens();
        var refreshPath = Actions.REFRESH

        if (!isRefreshing) {
          isRefreshing = true

          return store
            .dispatch(refreshPath, { tokenModel })
            .then(response => {
              isRefreshing = false
              console.log(response)
              return axios(originalRequest)
            })
            .catch(error => {
              isRefreshing = false
              console.log(error)
              // Logout
            })
        } else {
          console.log('XXXXX')
          console.log('SOME PROBLEM HERE') // <------------------
          console.log('XXXXX')
        }
      } else {
        store.commit(Mutations.SET_ERROR, error.response.data.error)
      }
      return Promise.reject(error)
    }
  )
}

I'm not sure what i need in the else block highlighted above.

EDIT:

When I do

return axios(originalRequest)

in the else block it works, however im not happy with the behaviours. It basically retries all the requests again and again until the token is refreshed. I would rather it just retried once after the token had been refreshed Any ideas

Thanks

like image 779
raklos Avatar asked May 27 '19 14:05

raklos


2 Answers

You can just have additional interceptor which can refresh token and execute your pending requests.

In this, countDownLatch class can help. Here is sample Interceptor code,

class AutoRefreshTokenRequestInterceptorSample() : Interceptor {

    companion object {
        var countDownLatch = CountDownLatch(0)
        var previousAuthToken = ""

        const val SKIP_AUTH_TOKEN = "SkipAccessTokenHeader"
        const val AUTHORIZATION_HEADER = "AUTHORIZATION_HEADER_KEY"
    }

    @Throws(IOException::class)
    override fun intercept(chain: Interceptor.Chain): Response? {
        val request = chain.request()

        if (shouldExecuteRequest(request)) {

            // Execute Request
            val response = chain.proceed(request)

            if (!response.isSuccessful) {
                // Failed Case
                val errorBody = response.peekBody(java.lang.Long.MAX_VALUE).string()
                val error = parseErrorModel(errorBody)

                // Gives Signal to HOLD the Request Queue
                countDownLatch = CountDownLatch(1)

                handleError(error!!)

                // After updating token values, execute same request with updated values.
                val updatedRequest = getUpdatedRequest(request)

                // Gives Signal to RELEASE Request Queue
                countDownLatch.countDown()

                //Execute updated request
                return chain.proceed(updatedRequest)
            } else {
                // success case
                return response
            }
        }

        // Change updated token values in pending request objects and execute them!
        // If Auth header exists, and skip header not found then hold the request
        if (shouldHoldRequest(request)) {
            try {
                // Make this request to WAIT till countdown latch has been set to zero.
                countDownLatch.await()
            } catch (e: Exception) {
                e.printStackTrace()
            }

            // Once token is Updated, then update values in request model.
            if (previousAuthToken.isNotEmpty() && previousAuthToken != "newAccessToken") {
                val updatedRequest = getUpdatedRequest(request)
                return chain.proceed(updatedRequest)
            }
        }

        return chain.proceed(request)
    }

    private fun handleError(error: ErrorDto) {
        // update your token as per your error code logic
        //Here it will make new API call to update tokens and store it in your local preference.
    }

    /***
     * returns Request object with updated token values.
     */
    private fun getUpdatedRequest(request: Request): Request {
        var updateAuthReqBuilder: Request.Builder = request.newBuilder()
        var url = request.url().toString()

        if (url.contains(previousAuthToken.trim()) && previousAuthToken.trim().isNotEmpty()) {
            url = url.replace(previousAuthToken, "newAccessToken")
        }
        updateAuthReqBuilder = updateAuthReqBuilder.url(url)
        // change headers if needed
        return updateAuthReqBuilder.build()
    }

    private fun shouldExecuteRequest(request: Request) =
            shouldHoldRequest(request) && isSharedHoldSignalDisabled()

    /**
     * If count down latch has any value then it is reported by previous request's error signal to hold the whole pending chain.
     */
    private fun isSharedHoldSignalDisabled() = countDownLatch.count == 0L

    private fun shouldHoldRequest(request: Request) = !hasSkipFlag(request) && hasAuthorizationValues(request)

    private fun hasAuthorizationValues(request: Request) = isHeaderExist(request, AUTHORIZATION_HEADER)

    private fun hasSkipFlag(request: Request) = isHeaderExist(request, SKIP_AUTH_TOKEN)


    private fun isHeaderExist(request: Request, headerName: String): Boolean {
        return request.header(headerName) != null
    }

    private fun parseErrorModel(errorBody: String): Error? {
        val parser = JsonParser()

        // Change this logic according to your requirement.
        val jsonObject = parser.parse(errorBody).asJsonObject
        if (jsonObject.has("Error") && jsonObject.get("Error") != null) {
            val errorJsonObj = jsonObject.get("Error").asJsonObject
            return decodeErrorModel(errorJsonObj)
        }
        return null
    }

    private fun decodeErrorModel(jsonObject: JsonObject): Error {
        val error = Error()
       // decode your error object here
        return error
    }
}
like image 137
Dhara Vamja Avatar answered Nov 07 '22 13:11

Dhara Vamja


This is how I do:

let isRefreshing = false;
let failedQueue = [];

const processQueue = (error, token = null) => {
  failedQueue.forEach(prom => {
    if (error) {
      prom.reject(error);
    } else {
      prom.resolve(token);
    }
  });
  failedQueue = [];
};

axios.interceptors.response.use(
    response => response,
    error => {
      const originalRequest = error.config;
      if (error.response.status === 400) {
        // If response is 400, logout
        store.dispatch(logout());
      }
      // If 401 and I'm not processing a queue
      if (error.response.status === 401 && !originalRequest._retry) {
        if (isRefreshing) {
          // If I'm refreshing the token I send request to a queue
          return new Promise((resolve, reject) => {
            failedQueue.push({ resolve, reject });
          })
            .then(() => {
              originalRequest.headers.Authorization = getAuth();
              return axios(originalRequest);
            })
            .catch(err => err);
        }
        // If header of the request has changed, it means I've refreshed the token
        if (originalRequest.headers.Authorization !== getAuth()) {
          originalRequest.headers.Authorization = getAuth();
          return Promise.resolve(axios(originalRequest));
        }

        originalRequest._retry = true; // mark request a retry
        isRefreshing = true; // set the refreshing var to true

        // If none of the above, refresh the token and process the queue
        return new Promise((resolve, reject) => {
          // console.log('REFRESH');
          refreshAccessToken() // The method that refreshes my token
            .then(({ data }) => {
              updateToken(data); // The method that sets my token to localstorage/Redux/whatever
              processQueue(null, data.token); // Resolve queued
              resolve(axios(originalRequest)); // Resolve current
            })
            .catch(err => {
              processQueue(err, null);
              reject(err);
            })
            .then(() => {
              isRefreshing = false;
            });
        });
      }

      return Promise.reject(error);
    },
  );
like image 41
Mosè Raguzzini Avatar answered Nov 07 '22 12:11

Mosè Raguzzini