61

Our React Native Redux app uses JWT tokens for authentication. There are many actions that require such tokens and a lot of them are dispatched simultaneously e.g. when app loads.

E.g.

componentDidMount() {
    dispath(loadProfile());
    dispatch(loadAssets());
    ...
}

Both loadProfile and loadAssets require JWT. We save the token in the state and AsyncStorage. My question is how to handle token expiration.

Originally I was going to use middleware for handling token expiration

// jwt-middleware.js

export function refreshJWTToken({ dispatch, getState }) {

  return (next) => (action) => {
    if (isExpired(getState().auth.token)) {
      return dispatch(refreshToken())
          .then(() => next(action))
          .catch(e => console.log('error refreshing token', e));
    }
    return next(action);
};

}

The problem that I ran into was that refreshing of the token will happen for both loadProfile and loadAssets actions because at the time when they are dispatch the token will be expired. Ideally I would like to "pause" actions that require authentication until the token is refreshed. Is there a way to do that with middleware?

lanan
  • 2,742
  • 3
  • 22
  • 29
  • 1
    I suggest you to look at a library called [redux-saga](https://github.com/yelouafi/redux-saga)... It solves this problem perfectly. – Kevin He May 09 '16 at 08:11
  • 4
    @KevinHe: can you share a bit more how redux-saga solves this problem? – eugene Dec 12 '18 at 10:01

4 Answers4

53

I found a way to solve this. I am not sure if this is best practice approach and there are probably some improvements that could be made to it.

My original idea stays: JWT refresh is in the middleware. That middleware has to come before thunk if thunk is used.

...
const createStoreWithMiddleware = applyMiddleware(jwt, thunk)(createStore);

Then in the middleware code we check to see if token is expired before any async action. If it is expired we also check if we are already are refreshing the token -- to be able to have such check we add promise for fresh token to the state.

import { refreshToken } from '../actions/auth';

export function jwt({ dispatch, getState }) {

    return (next) => (action) => {

        // only worry about expiring token for async actions
        if (typeof action === 'function') {

            if (getState().auth && getState().auth.token) {

                // decode jwt so that we know if and when it expires
                var tokenExpiration = jwtDecode(getState().auth.token).<your field for expiration>;

                if (tokenExpiration && (moment(tokenExpiration) - moment(Date.now()) < 5000)) {

                    // make sure we are not already refreshing the token
                    if (!getState().auth.freshTokenPromise) {
                        return refreshToken(dispatch).then(() => next(action));
                    } else {
                        return getState().auth.freshTokenPromise.then(() => next(action));
                    }
                }
            }
        }
        return next(action);
    };
}

The most important part is refreshToken function. That function needs to dispatch action when token is being refreshed so that the state will contain the promise for the fresh token. That way if we dispatch multiple async actions that use token auth simultaneously the token gets refreshed only once.

export function refreshToken(dispatch) {

    var freshTokenPromise = fetchJWTToken()
        .then(t => {
            dispatch({
                type: DONE_REFRESHING_TOKEN
            });

            dispatch(saveAppToken(t.token));

            return t.token ? Promise.resolve(t.token) : Promise.reject({
                message: 'could not refresh token'
            });
        })
        .catch(e => {

            console.log('error refreshing token', e);

            dispatch({
                type: DONE_REFRESHING_TOKEN
            });
            return Promise.reject(e);
        });



    dispatch({
        type: REFRESHING_TOKEN,

        // we want to keep track of token promise in the state so that we don't try to refresh
        // the token again while refreshing is in process
        freshTokenPromise
    });

    return freshTokenPromise;
}

I realize that this is pretty complicated. I am also a bit worried about dispatching actions in refreshToken which is not an action itself. Please let me know of any other approach you know that handles expiring JWT token with redux.

Sagar Guhe
  • 1,041
  • 1
  • 11
  • 35
lanan
  • 2,742
  • 3
  • 22
  • 29
  • You could make refreshToken receive a "postponedAction" that will be dispatched if the refresh is done successfully instead of returning a new Promise. At least that's how I solved this. – Matías Hernán García Aug 09 '16 at 19:26
  • @MatíasHernánGarcía - Do you have an example of that? – PI. Sep 06 '16 at 16:06
  • @PI I'm guessing @MatíasHernánGarcía meant something like (pseudo code): `export function refreshToken(postponedAction) { refreshSuccess.then(() => dispatch(postponedAction)) }` – Mark Murphy Apr 24 '17 at 14:25
  • 6
    @Shvetusya I wouldn't be worried about dispatching actions in refreshToken which is not an action itself. refreshToken is essentially an action creator and dispatching other actions in an actionCreator like this is pretty common practice – Mark Murphy Apr 24 '17 at 14:29
  • Hi, thanks for sharing this. Really helpful, but i got dispatch is undefined on refreshToken. any solution? – ssuhat Jun 25 '17 at 15:59
  • I'm trying to the the same thing you're doing here. One question - you said this will needed for each API call which requires sending a JWT, so how do you ensure an action is dispatched on each API call? Manually? – JamesG Jul 14 '17 at 16:14
  • @JamesG in my setup every api call is an action, e.g. `dispatch(loadProfile())` triggeres an API call to /profile/. By using `jwt` middleware before any async action that is dispatched I can check if the token is expired. – lanan Jul 14 '17 at 17:48
  • @Shvetusya what I'm struggling with here is, doesn't your refresh token call get hit by the same middleware and then stopped because of the current promise that you stored in the state? – Brandon Aug 11 '17 at 14:30
  • @Brandon I don't `dispatch` `refreshToken` call, it is not an action itself. See my last note in the post and comments above. This definitely feels a bit complicated but it works. – lanan Aug 15 '17 at 00:44
  • @Shvetusya I actually meant the `fetchJWTToken()` call! – Brandon Aug 19 '17 at 03:57
  • @Brandon same thing it is a regular function, not an action – lanan Aug 23 '17 at 17:29
  • 1
    Thx a lot for this piece of code! Maybe after all action, we need to remove freshTokenPromise object from the state? return getState() .auth.freshTokenPromise.then(() => next(action)) .then(() => { dispatch({ type: REFRESHING_TOKEN_PROMISE_CLEAN, freshTokenPromise: null, }) }) – Aleksey Makas Apr 10 '19 at 19:39
  • 3
    beautiful ! a little note for the ones with `redux-persist`, persisting a promise is not supported, `freshTokenPromise` has to be excluded/blacklisted with a transformer – Hatem Alimam Aug 21 '19 at 10:33
  • Thanks for your response, I have implemented your approach and it is the only solution I am aware of! At the same time, I wonder if it is [bad practice to put the promise in redux as it is not serializable?](https://stackoverflow.com/questions/42445336/should-i-store-promises-in-redux) – simondefreeze Apr 15 '20 at 03:41
  • @HatemAlimam How did you implement exclusion/blacklisting? Do you have any code snippet for that? – Jawla Apr 18 '20 at 11:05
  • 1
    @Jawla here's an example https://gist.github.com/hatemalimam/5e196f4953f50187b130600f62a99856 hope it helps – Hatem Alimam Apr 18 '20 at 11:10
22

Instead of "waiting" for an action to finish, you could instead keep a store variable to know if you're still fetching tokens:

Sample reducer

const initialState = {
    fetching: false,
};
export function reducer(state = initialState, action) {
    switch(action.type) {
        case 'LOAD_FETCHING':
            return {
                ...state,
                fetching: action.fetching,
            }
    }
}

Now the action creator:

export function loadThings() {
    return (dispatch, getState) => {
        const { auth, isLoading } = getState();

        if (!isExpired(auth.token)) {
            dispatch({ type: 'LOAD_FETCHING', fetching: false })
            dispatch(loadProfile());
            dispatch(loadAssets());
       } else {
            dispatch({ type: 'LOAD_FETCHING', fetching: true })
            dispatch(refreshToken());
       }
    };
}

This gets called when the component mounted. If the auth key is stale, it will dispatch an action to set fetching to true and also refresh the token. Notice that we aren't going to load the profile or assets yet.

New component:

componentDidMount() {
    dispath(loadThings());
    // ...
}

componentWillReceiveProps(newProps) {
    const { fetching, token } = newProps; // bound from store

    // assuming you have the current token stored somewhere
    if (token === storedToken) {
        return; // exit early
    }

    if (!fetching) {
        loadThings()
    } 
}

Notice that now you attempt to load your things on mount but also under certain conditions when receiving props (this will get called when the store changes so we can keep fetching there) When the initial fetch fails, it will trigger the refreshToken. When that is done, it'll set the new token in the store, updating the component and hence calling componentWillReceiveProps. If it's not still fetching (not sure this check is necessary), it will load things.

ZekeDroid
  • 7,089
  • 5
  • 33
  • 59
  • 2
    Thanks! This definitely makes sense for the initial load. But I am not sure if it works for expiring tokens after the app is loaded and is in use. Every call to the API requires valid token. We have many pop up views that require login and load data so I am not sure if handling expiration through props for those views would work. – lanan May 02 '16 at 16:01
  • You can change the logic to check for the expiration of the token instead of difference in token. The idea is that any action will trigger this lifecycle method so you can make use of it to update the `fetching` variable and react accordingly – ZekeDroid May 02 '16 at 16:19
  • 2
    My first issue with adding `dispatch({ type: 'LOAD_FETCHING', fetching: true })` to every action that requires JWT is code duplication. Second problem is how to know when the refresh completed. Say there is an "Add to Favourites" button that dispatches an api call that requires auth. Do I want to add "if token expired refresh then make a call" logic to that action? What about other similar actions? This is why I am trying to use middleware. In other frameworks / languages I have used decorators but I am not sure if I can do that with React. – lanan May 02 '16 at 17:02
  • Ah yes, it would get repetitive and definitely should be middleware. Decorators would make sense but I'm not sure you can use them either. One other strategy would be to 'queue' your actions, like `'ADD_TO_FAVS'`, into an queue array, by the middleware. Immediately try to dispatch but if the token is stale, refresh it. Meanwhile, subscribe to this change and on any change attempt to empty the queue. There will be a delay in the dispatching but no more than expected for this sort of handshaking. – ZekeDroid May 02 '16 at 19:21
8

I made a simple wrapper around redux-api-middleware to postpone actions and refresh access token.

middleware.js

import { isRSAA, apiMiddleware } from 'redux-api-middleware';

import { TOKEN_RECEIVED, refreshAccessToken } from './actions/auth'
import { refreshToken, isAccessTokenExpired } from './reducers'


export function createApiMiddleware() {
  const postponedRSAAs = []

  return ({ dispatch, getState }) => {
    const rsaaMiddleware = apiMiddleware({dispatch, getState})

    return (next) => (action) => {
      const nextCheckPostponed = (nextAction) => {
          // Run postponed actions after token refresh
          if (nextAction.type === TOKEN_RECEIVED) {
            next(nextAction);
            postponedRSAAs.forEach((postponed) => {
              rsaaMiddleware(next)(postponed)
            })
          } else {
            next(nextAction)
          }
      }

      if(isRSAA(action)) {
        const state = getState(),
              token = refreshToken(state)

        if(token && isAccessTokenExpired(state)) {
          postponedRSAAs.push(action)
          if(postponedRSAAs.length === 1) {
            return  rsaaMiddleware(nextCheckPostponed)(refreshAccessToken(token))
          } else {
            return
          }
        }
      
        return rsaaMiddleware(next)(action);
      }
      return next(action);
    }
  }
}

export default createApiMiddleware();

I keep tokens in the state, and use a simple helper to inject Acess token into a request headers

export function withAuth(headers={}) {
  return (state) => ({
    ...headers,
    'Authorization': `Bearer ${accessToken(state)}`
  })
}

So redux-api-middleware actions stays almost unchanged

export const echo = (message) => ({
  [RSAA]: {
      endpoint: '/api/echo/',
      method: 'POST',
      body: JSON.stringify({message: message}),
      headers: withAuth({ 'Content-Type': 'application/json' }),
      types: [
        ECHO_REQUEST, ECHO_SUCCESS, ECHO_FAILURE
      ]
  }
})

I wrote the article and shared the project example, that shows JWT refresh token workflow in action

MikeM
  • 13,156
  • 2
  • 34
  • 47
kmmbvnr
  • 5,863
  • 4
  • 35
  • 44
0

I think that redux is not the right tool for enforcing the atomicity of token refresh.

Instead I can offer you an atomic function that can be called from anywhere and ensures that you will always get a valid token:

/*
    The non-atomic refresh function
*/

const refreshToken = async () => {
    // Do whatever you need to do here ...
}

/*
    Promise locking-queueing structure
*/

var promiesCallbacks = [];

const resolveQueue = value => {
  promiesCallbacks.forEach(x => x.resolve(value));
  promiesCallbacks = [];
};
const rejectQueue = value => {
  promiesCallbacks.forEach(x => x.reject(value));
  promiesCallbacks = [];
};
const enqueuePromise = () => {
  return new Promise((resolve, reject) => {
    promiesCallbacks.push({resolve, reject});
  });
};

/*
    The atomic function!
*/

var actionInProgress = false;

const refreshTokenAtomically = () => {
  if (actionInProgress) {
    return enqueuePromise();
  }

  actionInProgress = true;

  return refreshToken()
    .then(({ access }) => {
      resolveQueue(access);
      return access;
    })
    .catch((error) => {
      rejectQueue(error);
      throw error;
    })
    .finally(() => {
      actionInProgress = false;
    });
};

Posted also here: https://stackoverflow.com/a/68154638/683763

ULazdins
  • 1,975
  • 4
  • 25
  • 31