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

1

34

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.
Villosity answered 19/8, 2018 at 7:5 Comment(1)
Great question, not sure how much I like the proposed solutions, what did you end up doing?Whoops
E
6

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.

Earmark answered 19/8, 2018 at 9:23 Comment(13)
Hmm interesting, thought I thought you would use an interval timer as maybe I am not seeing it but how do your keep your timer going? I guess since access tokens are accepted as long as they are expired, even if a new token is created while an old one is sent out it would still be allowed through? How does your code handle if say a refresh token was invalidated and you should actually force them out of your site? Still checking for 401?Villosity
If it is not too much to show, I would like to see how the abort would work as I am thinking if you abort them all, would you actually resend the requests wants a new token was made?Villosity
I would like to answer both of your concerns; - Timer => There's some implementation on backend, I get remaining expiry time of that token in response of validation API . Even App gets reloaded and it sends the validate API request again and get remaining expiry time as we're managing expiry time in backend. So I set a SetTImeOut of 1 minute before the actual expiry time. It accepts the refresh token and I get new access token and refresh token. Then I update locatStoarge So all APIs consume latest access token in their header. You don't need to check 401in all APIs.Earmark
For your second question regarding aborting requests, you can see my EDITED answer above.Earmark
Interesting. So you went option 1 right? Why? For the first way are you checking still for 401 to redirect to login?Villosity
Yes I went for option 1. I find it way more smarter approach just to make things clear. No I check only 400 for bad requests to navigate to login for unauthorised users. My case is different I’m using webview in android and iOS and I get first access token from native app on every app reload. Both approached are good. I would suggest go with the option which is more appropriate with your case. If you have any question you can ping me anytime.Earmark
I went with option 1 as well. I am just having problems when say they close down the browser and then come back via a copied link. So I put my code to check if they have an authentication in my app.js . I get a redirect to my login as the component they are going to runs before the app.js mounting so an ajax happens and it comes back with a 401. I need some event that fires before everything else.Villosity
@Villosity you should have a parent component of your all routes which should be mounted on first and you should check authentication in componentDidMount. I'm doing this right nowEarmark
Well, app.js is my root component, but I do have a few other components that have routes in it(member component, admin component). So maybe i need to add my code to each of these?Villosity
hmm, I am looking at my code. I have MembersArea that has route to my admin section, but when I directly type in the url to the admin area, the admin component runs before the memberarea componentVillosity
@Villosity There should be a component above on all routes which will be rendered first if you load the app with any url.Earmark
Well that should be my app.js as that is the frist thing I do <Provider routingStore={routingStore} domainStores={DomainStores} uiStores={uiStores}> <Router history={history}> <AppContainer /> </Router> </Provider>,Villosity
Let us continue this discussion in chat.Earmark

© 2022 - 2024 — McMap. All rights reserved.