I’m trying to write a response interceptor for my React project but I am having some issues.
When a user gets a 401 from their original request I want to try and refresh the token and continue, but if the user gets a 401 from their original request and when trying to refresh the token it fails then redirect them to the login page.
What I have does the first bit just fine, it refreshes the token and continues with the original request, but the issue i am having is that if the refresh fails, its not redirecting the user to the login page.
I would love some input on what I am doing wrong
import axios from 'axios';
import { useRouter } from 'next/router'
const router = useRouter();
const apiInstance = axios.create({
baseURL: process.env.API_URL
});
apiInstance.interceptors.response.use((response) => {
return response;
}, async function (error) {
const originalRequest = error.config;
if (error.response.status === 401 && originalRequest.url === '/oauth/token') {
router.push('/');
return Promise.reject(error);
}
if (error.response.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;
return axios.post(process.env.OAUTH_BASE_URL + '/oauth/token', {
'grant_type': 'refresh_token',
'refresh_token': localStorage.getItem('refresh_token'),
'client_id': process.env.CLIENT_ID,
})
.then(res => {
if (res.status === 200) {
localStorage.setItem('access_token', res.access_token);
localStorage.setItem('refresh_token', res.refresh_token);
localStorage.setItem('expires_in', res.expires_in);
axios.defaults.headers.common['Authorization'] = 'Bearer ' + localStorage.getItem('access_token');
return apiInstance(originalRequest);
}
})
}
return Promise.reject(error);
});
export default apiInstance;
Advertisement
Answer
There’s a couple of errors here. First, url property is equal to the whole value of url param of axios call, so this…
originalRequest.url === '/oauth/token'
… is only true if process.env.OAUTH_BASE_URL is an empty string (and most likely it’s not). In general, it’s better to avoid checking against URLs and use flags/custom properties set on request objects (as with _retry flag).
Also, note that while apiInstance is used for regular API call, the particular call for refresh token actually avoids it:
return axios.post(process.env.OAUTH_BASE_URL + '/oauth/token', { //
^^^^^^^^^^
… which means interceptors for this call are not even fired.
Here’s one possible approach to solve this. apiInstance here is the exported axios instance, and setTokens/getAccessToken/getRefreshToken are simple abstractions over mechanisms of storing/retrieving particular tokens.
apiInstance.interceptors.request.use(request => {
if (!request._refreshToken) {
request.headers.Authorization = 'Bearer ' + getAccessToken();
}
// console.log('REQUEST', request.method + ' ' + request.url);
return request;
});
apiInstance.interceptors.response.use(
void 0, // better skip this argument altogether
error => {
const originalRequest = error.config;
if (originalRequest._refreshToken) {
console.log('REFRESH TOKEN FAILED');
// ... and all the things you need to do when refreshing token failed,
// like resettting access token, and rerouting users to /login page,
// or just sending an event for Router to process
return Promise.reject(error);
}
const errorResponse = error.response;
if (errorResponse.status !== 401) {
return Promise.reject(error);
}
return apiInstance.post('/oauth/token', {
grant_type: 'refresh_token',
refresh_token: getRefreshToken(),
client_id: process.env.CLIENT_ID,
}, {
_refreshToken: true // custom parameter
}).then((resp) => {
setTokens(resp.data);
return apiInstance(originalRequest);
});
}
);
There are two ideas behind this (easily testable with unit tests): first, failed refresh token requests always stop the interceptor chain (as they throw immediately), second, if ‘business-level’ API request fails, it’s always preceded with refresh-token one.
Note that this code is just a prototype to illustrate the concept here. If you expect your code to be able to issue multiple API calls at once, token refresh should actually be wrapped into a function returning single promise (to avoid subsequent refresh-token calls). If you’re going to use this in production, I strongly suggest at least considering using axios-auth-refresh instead of writing your own implementation for that.