React native Refresh works but next call still uses the last token
Asked Answered
D

3

16

I use the following middleware to refresh my token when it expires :

import {AsyncStorage} from 'react-native';
import moment from 'moment';
import fetch from "../components/Fetch";
import jwt_decode from 'jwt-decode';

/**
 * This middleware is meant to be the refresher of the authentication token, on each request to the API,
 * it will first call refresh token endpoint
 * @returns {function(*=): Function}
 * @param store
 */
const tokenMiddleware = store => next => async action => {
  if (typeof action === 'object' && action.type !== "FETCHING_TEMPLATES_FAILED") {
    let eToken = await AsyncStorage.getItem('eToken');
    if (isExpired(eToken)) {
      let rToken = await AsyncStorage.getItem('rToken');

      let formData = new FormData();
      formData.append("refresh_token", rToken);

      await fetch('/token/refresh',
        {
          method: 'POST',
          body: formData
        })
        .then(response => response.json())
        .then(async (data) => {
            let decoded = jwt_decode(data.token);
            console.log({"refreshed": data.token});

            return await Promise.all([
              await AsyncStorage.setItem('token', data.token).then(() => {return AsyncStorage.getItem('token')}),
              await AsyncStorage.setItem('rToken', data.refresh_token).then(() => {return AsyncStorage.getItem('rToken')}),
              await AsyncStorage.setItem('eToken', decoded.exp.toString()).then(() => {return AsyncStorage.getItem('eToken')}),
            ]).then((values) => {
              return next(action);
            });
        }).catch((err) => {
          console.log(err);
        });

      return next(action);
    } else {
      return next(action);
    }
  }

  function isExpired(expiresIn) {
    // We refresh the token 3.5 hours before it expires(12600 seconds) (lifetime on server  25200seconds)
    return moment.unix(expiresIn).diff(moment(), 'seconds') < 10;
  }
};
  export default tokenMiddleware;

And the fetch helper :

import { AsyncStorage } from 'react-native';
import GLOBALS from '../constants/Globals';
import {toast} from "./Toast";
import I18n from "../i18n/i18n";

const jsonLdMimeType = 'application/ld+json';

export default async function (url, options = {}, noApi = false) {
  if ('undefined' === typeof options.headers) options.headers = new Headers();
  if (null === options.headers.get('Accept')) options.headers.set('Accept', jsonLdMimeType);

  if ('undefined' !== options.body && !(options.body instanceof FormData) && null === options.headers.get('Content-Type')) {
    options.headers.set('Content-Type', jsonLdMimeType);
  }

  let token = await AsyncStorage.getItem('token');
  console.log({"url": url,"new fetch": token});
  if (token) {
    options.headers.set('Authorization', 'Bearer ' + token);
  }

  let api = '/api';

  if (noApi) {
    api = "";
  }

  const link = GLOBALS.BASE_URL + api + url;
  return fetch(link, options).then(response => {
    if (response.ok) return response;

    return response
      .json()
      .then(json => {
        if (json.code === 401) {
          toast(I18n.t(json.message), "danger", 3000);
          AsyncStorage.setItem('token', '');
        }

        const error = json['message'] ? json['message'] : response.statusText;
        throw Error(I18n.t(error));
      })
      .catch(err => {
        throw err;
      });
  })
  .catch(err => {
    throw err;
  });
}

My issue is :

  • when I make an action, the middleware is called.
  • If the token is about to expire, then the refresh token method is called and the AsyncStorage is updated.
  • Then the next(action) method is supposed to be called.
  • But my /templates endpoint is called before (not after) my /token/refresh endpoint using the old expired token...
  • Then the consequence is that my current screen return an error (Unauthorized) but if the user changes screen it will be work again since its token have successfully been refreshed. But it's ugly that way :p

EDIT : For the sake of this issue, I've rework my code to put this into one file. I've also put some console.log to show how this code will be executed

Execution queue

We can see from the image that :

  • My calls (/templates) are exectued before my refresh endpoint. And my console log of the refreshed token arrives long after that...

Any help on this please ?

EDIT until the end of the bounty :

From that question I try to understand why my approach is wrong about middleware since many of ressources I found on internet talk about middleware as the best solution to achieve refresh token operations.

Dessau answered 19/2, 2019 at 15:51 Comment(0)
S
3

I have a slightly different setup in handling. Instead of handling the refresh token logic in middleware, I define it as helper function. This way I can do all token validation right before any network request where I see fit, and any redux action that doesn't involves a network request will not needed this function

export const refreshToken = async () => {
  let valid = true;

  if (!validateAccessToken()) {
    try {
      //logic to refresh token
      valid = true;
    } catch (err) {
      valid = false;
    }

    return valid;
  }
  return valid;
};

const validateAccessToken = () => {
  const currentTime = new Date();

  if (
    moment(currentTime).add(10, 'm') <
    moment(jwtDecode(token).exp * 1000)
  ) {
    return true;
  }
  return false;
};

Now that we have this helper function, I call it for all redux action that needed

const shouldRefreshToken = await refreshToken();
    if (!shouldRefreshToken) {
      dispatch({
        type: OPERATION_FAILED,
        payload: apiErrorGenerator({ err: { response: { status: 401 } } })
      });
    } else { 
      //...
    }
Subcontinent answered 22/2, 2019 at 16:35 Comment(2)
Hi thanks for your suggestion, it may work of course, can you explain why my method not working ? Your solution involve a lot of condition in my reducers wich I don't want to do if I have another option. Middleware are suppose to work right ?Dessau
For your pragmatism, and the solution, you'll have the correct answer. I tested it and it worked my problem is solved using your way.Dessau
O
3

In your middleware you are making store.dispatch asynchronous, but the original signature of store.dispatch is synchronous. This can have serious side effects.

Let's consider a simple middleware, that logs every action that happens in the app, together with the state computed after it:

const logger = store => next => action => {
  console.log('dispatching', action)
  let result = next(action)
  console.log('next state', store.getState())
  return result
}

Writing the above middleware is essentially doing the following:

const next = store.dispatch  // you take current version of store.dispatch
store.dispatch = function dispatchAndLog(action) {  // you change it to meet your needs
  console.log('dispatching', action)
  let result = next(action) // and you return whatever the current version is supposed to return
  console.log('next state', store.getState())
  return result
}

Consider this example with 3 such middleware chained together:

const {
  createStore,
  applyMiddleware,
  combineReducers,
  compose
} = window.Redux;

const counterReducer = (state = 0, action) => {
  switch (action.type) {
    case "INCREMENT":
      return state + 1;

    default:
      return state;
  }
};

const rootReducer = combineReducers({
  counter: counterReducer
});


const logger = store => next => action => {
  console.log("dispatching", action);
  let result = next(action);
  console.log("next state", store.getState());
  return result;
};

const logger2 = store => next => action => {
  console.log("dispatching 2", action);
  let result = next(action);
  console.log("next state 2", store.getState());
  return result;
};

const logger3 = store => next => action => {
  console.log("dispatching 3", action);
  let result = next(action);
  console.log("next state 3", store.getState());
  return result;
};

const middlewareEnhancer = applyMiddleware(logger, logger2, logger3);

const store = createStore(rootReducer, middlewareEnhancer);

store.dispatch({
  type: "INCREMENT"
});

console.log('current state', store.getState());
<script src="https://unpkg.com/[email protected]/dist/redux.js"></script>

First logger gets the action, then logger2, then logger3 and then it goes to the actual store.dispatch & the reducer gets called. The reducer changes the state from 0 to 1 and logger3 gets the updated state and propagates the return value (the action) back to logger2 and then logger.

Now, lets consider what happens when you change the store.dispatch to a async function somewhere in the middle of the chain:

const logger2 = store => next => async action => {
  function wait(ms) {
    return new Promise(resolve => {
      setTimeout(() => {
        resolve();
      }, ms);
    });
  }
  await wait(5000).then(v => {
    console.log("dispatching 2", action);
    let result = next(action);
    console.log("next state 2", store.getState());
    return result;
  });
};

I have modified logger2, but logger (the one up the chain) has no idea that the next is now asynchronous. It will return a pending Promise and will come back with the "unupdated" state because the dispatched action had not reached the reducer yet.

const {
  createStore,
  applyMiddleware,
  combineReducers,
  compose
} = window.Redux;

const counterReducer = (state = 0, action) => {
  switch (action.type) {
    case "INCREMENT":
      return state + 1;

    default:
      return state;
  }
};

const rootReducer = combineReducers({
  counter: counterReducer
});


const logger = store => next => action => {
  console.log("dispatching", action);
  let result = next(action); // will return a pending Promise
  console.log("next state", store.getState());
  return result;
};

const logger2 = store => next => async action => {
  function wait(ms) {
    return new Promise(resolve => {
      setTimeout(() => {
        resolve();
      }, ms);
    });
  }
  await wait(2000).then(() => {
    console.log("dispatching 2", action);
    let result = next(action);
    console.log("next state 2", store.getState());
    return result;
  });
};

const logger3 = store => next => action => {
  console.log("dispatching 3", action);
  let result = next(action);
  console.log("next state 3", store.getState());
  return result;
};

const middlewareEnhancer = applyMiddleware(logger, logger2, logger3);

const store = createStore(rootReducer, middlewareEnhancer);

store.dispatch({ // console.log of it's return value is too a pending `Promise`
  type: "INCREMENT"
});

console.log('current state', store.getState());
<script src="https://unpkg.com/[email protected]/dist/redux.js"></script>

So my store.dispatch returns immediately from the chain of middleware with that pending Promise and console.log('current state', store.getState()); still prints 0. The action reaches original store.dispatch and the reducer looong after that.


I don't know your whole setup, but my guess is something like that is happening in your case. You are assuming your middleware has done something and made the round trip, but actually it hasn't finished the job (or no one awaited him to finish it). May be you are dispatching an action to fetch /templates and since you wrote a middleware to auto update the bearer token, you are assuming the fetch helper utility will be called with a brand new token. But the dispatch has returned early with a pending promise and your token is still the old one.

Apart from that, only one thing seems wrong visibly: you are dispatching the same action twice in your middleware via next:

const tokenMiddleware = store => next => async action => {
  if (something) {
    if (something) {
      await fetch('/token/refresh',)
        .then(async (data) => {
            return await Promise.all([
              // ...
            ]).then((values) => {
              return next(action); // First, after the `Promise.all` resolves
            });
        });
      return next(action); // then again after the `fetch` resolves, this one seems redundant & should be removed
    } else {
      return next(action);
    }
  }

Recommendations:

  1. Keep your tokens in redux store, persist them in storage and re-hydrate the redux store from storage
  2. Write one Async Action Creator for all api calls, that will refresh the token if necessary and dispatch an action asynchronously only after token has been refreshed.

Example with redux thunk:

function apiCallMaker(dispatch, url, actions) {
  dispatch({
    type: actions[0]
  })

  return fetch(url)
    .then(
      response => response.json(),
      error => {
        dispatch({
          type: actions[2],
          payload: error
        })
      }
    )
    .then(json =>
      dispatch({
        type: actions[1],
        payload: json
      })
    )
  }
}

export function createApiCallingActions(url, actions) {
  return function(dispatch, getState) {

    const { accessToken, refreshToken } = getState();
    if(neededToRefresh) {
      return fetch(url)
        .then(
          response => response.json(),
          error => {
            dispatch({
              type: 'TOKEN_REFRESH_FAILURE',
              payload: error
            })
          }
        )
        .then(json =>
          dispatch({
              type: 'TOKEN_REFRESH_SUCCESS',
              payload: json
          })
          apiCallMaker(dispatch, url, actions)
        )
    } else {
      return apiCallMaker(dispatch, url, actions)
    }
}

You would use it like so:

dispatch(createApiCallingActions('/api/foo', ['FOO FETCH', 'FOO SUCCESS', 'FOO FAILURE'])

dispatch(createApiCallingActions('/api/bar', ['BAR FETCH', 'BAR SUCCESS', 'BAR FAILURE'])
Occurrence answered 27/2, 2019 at 20:22 Comment(3)
whoa let me tell you that I'm glad of your answer, will definitely try this tomorrow. Thanks a lot for the time to write this.Dessau
I use redux-persist, to store some of my reducers. Your recommendation number one is tricky :p. Your apiCallMaker has an error right ? on error you canna call action[2] not [1] ?Dessau
For your explaination, You earned the bounty. Indeed it was my issue. But It means too much refactoring to solve it, I prefered to implement another solution (quickest, time is money) Thanks for your work on this.Dessau
A
1

You have a race condition of requests and there is no right solution which will totally solve this problem. Next items can be used as a starting point for solving this issue:

  • Use token refresh separately and wait for its execution on the client side, e.g. send token refresh (smth like GET /keepalive) in case any request was sent in half period of the session timeout - this will lead to the fact that all requests will be 100% authorized (Option that I'd definitely use - it can be also used to track not only requests but events)
  • Cleanup token after receiving 401 - you won't see working application after reload assuming that deletion of valid token in case of boundary scenarios is positive scenario (Simple to implement solution)
  • Repeat query that received 401 with some delay (not the best option actually)
  • Force token updates more frequently then the timeout - changing them at 50-75% of timeout will reduce amount of failing requests (but they will still persist if user was iddle for the all session time). So any valid request will return new valid token which will be used instead of the old one.

  • Implement token extension period when old token can be counted valid for the transfer period - old token is extended for some limited time in order to bypass the problem (sounds not very good but it is an option at least)

Auctorial answered 22/2, 2019 at 16:19 Comment(1)
Hi, I already considered most of your options. Actually I'm looking for a solution using my context (middleware) or really close solution.Dessau

© 2022 - 2024 — McMap. All rights reserved.