How to Handle Refresh Token When Multiple Requests are going out?

chobo2 picture chobo2 · Aug 19, 2018 · Viewed 13.6k times · Source

I am using reactjs, mbox and axios and ran into a problem. I have a api that gives out an access token and a refresh token. The access token dies every 20mins and when this happens the server sends a 401 back and my code will automatically send the refresh token out to get a new access token.

Once a new access token is granted that same rejected request will be sent again. Now my code works great until I throw multiple rejects that pretty much could fire all at the same time.

So first request goes off, a 401 is sent back and it gets a new refresh token, well all the other requests will be trying to do the same thing but the other requests will now fail because the refresh token will be used and a new one will be issued to the first request.

This will kick off my code to redirect the user to the login page.

So essentially I am stuck of only have 1 request at a time.

export const axiosInstance = axios.create({
    baseURL: getBaseUrl(),
    timeout: 5000,
    contentType: "application/json",
    Authorization: getAuthToken()
  });

  export function updateAuthInstant() {
    axiosInstance.defaults.headers.common["Authorization"] = getAuthToken();
  }


function getAuthToken() {
    if (localStorage.getItem("authentication")) {
      const auth = JSON.parse(localStorage.getItem("authentication"));
      return `Bearer ${auth.accessToken}`;
    }
  }

axiosInstance.interceptors.response.use(
  function(response) {
    return response;
  },
  function(error) {
    const originalRequest = error.config;
    if (error.code != "ECONNABORTED" && error.response.status === 401) {
      if (!originalRequest._retry) {
        originalRequest._retry = true;
        return axiosInstance
          .post("/tokens/auth", {
            refreshToken: getRefreshToken(),
            grantType: "refresh_token",
            clientId : "myclient"
          })
          .then(response => {

            uiStores.authenticaionUiStore.setAuthentication(JSON.stringify(response.data))
            updateAuthInstant();
            return axiosInstance(originalRequest);
          });
      } else {
        uiStores.authenticaionUiStore.logout();
        browserHistory.push({ pathname: '/login',});
      }

    }
    return Promise.reject(error);
  }
);

Edit

I am having problem that the code I Need to check to resetup authentication is not working when a user copies in a direct url

app.js

  <React.Fragment>
       <Switch>
          <Route path="/members" component={MemberAreaComponent} />
        </Switch>
  </React.Fragment >

In memberAreaComponent

      <Route path="/members/home" component={MembersHomeComponent} />

When I type in http://www.mywebsite/members/home

MembersHomeComponent - componentDidMount runs first
MemberAreaComponent - componentDidMount runs second
AppCoontainer = componentDidMount runs last.

Answer

Sakhi Mansoor picture Sakhi Mansoor · Aug 19, 2018

Hi I have implemented same scenario in react/redux app. But it would help you to achieve the goal. You don't need to check 401 in each API call. Just implement it in your first validation API request. You can use setTimeOut to send refresh token api request before some time of authentication token expiry. So locatStorage will get updated and All axios requests won't get expired token ever. Here is my solution:

in my Constants.js I;m maintaining USER TOKEN in localStorage like this:

 export const USER_TOKEN = {
   set: ({ token, refreshToken }) => {
      localStorage.setItem('access_token', token);
      localStorage.setItem('refresh_token', refreshToken);
   },
   remove: () => {
      localStorage.removeItem('access_token');
      localStorage.removeItem('refresh_token');
 },
   get: () => ({
     agent: 'agent',
     token: localStorage.getItem('access_token'),
     refreshToken: localStorage.getItem('refresh_token'),
  }),
   get notEmpty() {
      return this.get().token !== null;
  },
};

export const DEFAULT_HEADER = {
     get: () => ({
      'Content-type': 'application/json;charset=UTF-8',
       agent: `${USER_TOKEN.get().agent}`,
       access_token: `${USER_TOKEN.get().token}`,
 }),
};

on page load, User Validate API request is as follows:

dispatch(actions.validateUser(userPayload)) // First time authentication with user credentials and it return access token, refresh token and expiry time
  .then(userData => {
    const { expires_in, access_token, refresh_token } = userData
    USER_TOKEN.set({          // setting tokens in localStorage to accessible to all API calls
      token: access_token,
      refreshToken: refresh_token,
    });
    const timeout = expires_in * 1000 - 60 * 1000; // you can configure as you want but here it is 1 min before token will get expired
    this.expiryTimer = setTimeout(() => {  // this would reset localStorage before token expiry timr
      this.onRefreshToken();
    }, timeout);
  }).catch(error => {
    console.log("ERROR", error)
  });

onRefreshToken = () => {
   const { dispatch } = this.props;
   const refresh_token = USER_TOKEN.get().refreshToken;
   dispatch(actions.refreshToken({ refresh_token })).then(userData => {
      const { access_token, refresh_token } = userData
      USER_TOKEN.set({
         token: access_token,
          refreshToken: refresh_token,
    });
  });
};

Feel free to ask any questions, The other way is to implement axios abort controller to cancel pending promises. Happy to help with that too !

EDITED - You can maintain axios token source in all you API requests to abort them anytime. maintain axios token source in all of your apis. once you get first promise resolved then you can cancel all other pending APIs request. You can invoke onAbort method in after your first promise gets resolved. See this:

//in your component
class MyComponent extends Component{
isTokenSource = axios.CancelToken.source(); // a signal you can point to any API

componentDidMount{
   // for example if you're sending multiple api call here
        this.props.dispatch(actions.myRequest(payload, this.isTokenSource.token))
        .then(() => {
            // all good
        })
        .catch(error => {
            if (axios.isCancel(error)) {
                console.warn('Error', error);
            }
        });
}

onAbortStuff = () => {  // cancel request interceptor
    console.log("Aborting Request");
    this.isTokenSource.cancel('API was cancelled'); // This will abort all the pending promises if you send the same token in multiple requests, 
}

render(){
//
}

While in your axios request you can send token like this:

export const myRequest= (id, cancelToken) => {
    const URL = `foo`;
    return axios(URL, {
      method: 'GET',
      headers: DEFAULT_HEADER.get(),
      cancelToken: cancelToken
    })
.then(response => {
  // handle success
  return response.data;
  })
.catch(error => {
  throw error;
   });
  };

For reference you can this article it is very helpful in understanding of cancel subscriptions. https://medium.freecodecamp.org/how-to-work-with-react-the-right-way-to-avoid-some-common-pitfalls-fc9eb5e34d9e

You can do your routes structuring in this way: index.js

<Provider store={store}>
  <BrowserRouter>
    <App />
  </BrowserRouter>
</Provider>

App.js:

class App extends Component {


state = {
    isAuthenticated: false,
  };

  componentDidMount() {
   //authentication API and later you can setState isAuthenticate
   }
    render() {
    const { isAuthenticated } = this.state;
    return isAuthenticated ? <Routes /> : <Loading />;
  }

If you still find any issue, I'm more than happy to help you with this.