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.
So the answer is, Yes you can (and probably should) wait until your access token expires, and then refresh it.
Because the token is used to identify the client, if one is stolen or compromised, an attacker has full access to the user's account in the same way they would if the attacker had instead compromised the user's username and password. Refresh tokens are long-lived.
The refresh token is set with a very long expiration time of 200 days. If the traffic to this API is 10 requests/second, then it can generate as many as 864,000 tokens in a day.
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.
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