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 :
next(action)
method is supposed to be called./templates
endpoint is called before (not after) my /token/refresh
endpoint using the old expired token...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
We can see from the image 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.
We need to do 2 steps: – Create a component with react-router subscribed to check JWT Token expiry. – Render it in the App component. In src folder, create common/AuthVerify.
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 {
//...
}
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 await
ed 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:
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'])
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:
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)
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