Automate OAuth refresh token flow using Axios interceptors in ReactJs, React Native, or Javascript

OAuth 2.0 is a very common authentication mechanism that is used for validating the user and giving access to the resources. It is important for most of the client applications to provide support for the OAuth-based flow and one major task is to refresh the token and re-initiate a call.

In this post, we will see how to automate the process of refreshing a token and retry a previously failed request automatically using the interceptors in the Axios library.

OAuth Refresh Token Flow

OAuth is an industry-standard open authentication mechanism that provides specifications for the authentication of the users and authorization to resources using a token with expiry rather than passing the username password for each request. The general idea is to have the user enter the username password one time and retrieve an access_token from the auth server and use that to make calls to authenticated resources in resource servers.

There are different flows possible in OAuth and the general structure will contain an access_token and a refresh_token.

  • access_token is generally a short expiry token that is used to get access to the resources.
  • refresh_token will be having a larger expiry and can be used to request for a new access_token without the user requiring to enter the username and password.

Most of the web applications and mobile apps have their users login one time using the username password and retrieves the access_token and refresh_token. This will be used for subsequent authentication as long as the refresh_token is valid. The username and the password are not stored in the application thus reducing the risk of password theft.

Now the downside of this approach is that, when you call a resource ( API ) using an expired access_token, you will get a 401 response and it fails. At this point, you are required to call the refresh_token API and get a new access_token and re-initiate the request.

We will see how to automate this step using Axios interceptors so that you don’t need to bother about the retries and expired tokens.

Refresh token flow using Axios interceptors.

Axios is a widely popular promise-based HTTP client library that is used in ReactJs, React native, and other Javascript-based frameworks.

One of the powerful features of Axios is the availability of interceptors that allows us to intercept request and response. We can use this feature to inject our own logic and check before it is passed down.

Before you proceed further, make sure that you have installed Axios as a dependency on your project ( whether it’s React-based or any other javascript-based library ).

We will be leveraging the interceptors for creating a wrapper service for calling the APIs that handle the refresh_token flow.

Basic flow

  1. Create a response interceptor that checks if the response HTTP status code is 401 ( assuming to be a expired token )
  2. Store the original request.
  3. Extract the refresh_token ( local storage, cookies etc )
  4. Call the refresh_token API end point
  5. Verify that we received an access_token
  6. Use the new access_token and re-initiate the call again
  7. If we don’t receive a token when we initiate a refresh_token call, need to redirect the user to re-login.

Pitfalls to handle

  1. We should be able to differentiate between the 401 status code due to a invalid username password ( call to the auth server end point for first time authentication ) and the one due to expiry token ( from the resource API ).
  2. The retry should not be infinite. We should not retry again for a retry that is failed.

With these points in mind, we have created the below sample implementation.

Note that in the below example, the tokens are assumed to be stored as httpOnly cookies that gets automatically attached to every request and is parsed by the server. If you use a different mechanism for storing the tokens ( like localStorage ), you may need to modify the code to retrieve the token from there and pass along in the request as a header or param.

import axios from 'axios';

// Define the constants
export const BASE_URL = "http://develop.microideation.local:8080/gateway/"; 
export const URL_USER_AUTHENTICATE= "api/web/authenticate";
export const URL_REFRESH_TOKEN="api/web/refresh_token";

// Define the miAPI as axios
const miAPI = axios.create({
    baseURL: BASE_URL,
    withCredentials:true
});

// Add the interceptor for the response to handle the authentication issues
// This interceptor will check if the response status is 401 and will store the 
// current request. On 401, it will call the refresh token API and try to restore 
// the token. On success, we will post the original request again.
miAPI.interceptors.response.use(function(response) {

    // For success return the response as is
    return response;

},function(error) {

    // Log the error
    console.log("error :" + JSON.stringify(error));

    // Store the original request
    const originalReq = error.config;

    // Check if the response is having error code as 401
    // and is not a retry (to avoid infinite retries) and 
    // avoid checking on the authenticate URL as it may be due to user
    // entering wrong password.
    if ( error.response.status == 401 && 
         !originalReq._retry && 
         error.response.config.url != URL_USER_AUTHENTICATE ) {

        // Set the retry flag to true
        originalReq._retry = true;

        // Call the refresh_token API
        return axios.post(BASE_URL+URL_REFRESH_TOKEN,{})
                    .then((res) =>{
                        
                        // If the response is success , then log
                        if ( res.data.status == "success") {

                            // Log the message
                            console.log("token refreshed");

                            // Return the original request. ie. retry
                            return axios(originalReq);

                        } 
                    }).catch((error) => {window.location.href="/logout/nosession"});
    }

    // Log
    console.log("Rest promise error");
    
    // If not matched , then return the error
    return Promise.reject(error);
});

export {miAPI};

We have done the following in the code

  • Create miAPI as a wrapper object for Axios with base URL and credentials true ( this is for using the cookies based token storage ) . If you are using localStorage, you may skip this.
  • Create an interceptor for the response and
    • If the response code is fine, then return as is
    • If the response is error then,
      • Store the originalReq ( available as errorObj.config )
      • Check the status code is 401 and that it’s not a retry and also we are not calling the authenticate API ( differentiation for invalid credentials )
        • Call the refresh token API ( again, in my code the refresh token is passed as a cookie and if you have it stored else where you should retrieve it and pass as param to this API call ).
        • On success, update the access_token received ( In my case, since it’s a cookie based auth, the tokens will be set on httpOnly secure cookies. If you are using localStorage, you may parse it from response and store in localStorage or your preferred token store).
        • return the axios instance with original request ( retry original request )
        • On error , we redirect the user to logout screen
  • On interceptor error call the Promise.reject()

Now, whenever we use the miAPI Axios instance for calling the APIs, this interceptor will be invoked on receiving a response. It will check for 401 error code and if it’s not due to the authenticate API ( login ), we will try to make call refresh_token API and on success, we will retry the original request using the new access_token received. All these happen in the background and do not need to be handled on each call.

Using the new service for making calls

Let’s see how to use our new wrapper Axios instance miAPI for making REST calls.

import { miAPI, URL_CREATE_IDEATION } from "../../support/RestAPISpec";

// Call the service
miAPI
  .post(URL_CREATE_IDEATION, { ...ideationObj })
  .then((result) => {
    if (result.data.status === "success") {
      console.log("success");
    } else {
      console.log("failed");
    }
  })
  .catch((error) => {
    console.log("failed");
  })
  .then(console.log("completed"));

As you can see, we are importing and using the new wrapper object of Axios and making the REST GET or POST calls as required. All the options and the other functionalities will remain the same and behind the scenes, our interceptor will be handling the refresh_token expiry cases.

Note that in the above implementation, as the token is stored as httpOnly cookies, it will be already attached as part of the request which will be extracted by the server. But if you require the token to be stored and extracted from a localStorage, you could do so and either pass it as a header on each request or create a request interceptor for injecting it to all the requests.

You may also like...

Leave a Reply

Your email address will not be published. Required fields are marked *