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.