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?
This allows you to have short-lived access tokens without having to collect credentials every time one expires. Since access tokens aren't valid for an extended period because of security reasons, a refresh token helps to re-authenticate a user without login credentials.
For the refresh token, we will simply generate a UID and store it in an object in memory along with the associated user username. It would be normal to save it in a database with the user's information and the creation and expiration date (if we want it to be valid for a limited period of time).
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With