Skip to content

Instantly share code, notes, and snippets.

@mhm13dev
Forked from Godofbrowser/axios.refresh_token.1.js
Last active August 20, 2023 10:21
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mhm13dev/45d78ba2ab1024ba99416171af7214fb to your computer and use it in GitHub Desktop.
Save mhm13dev/45d78ba2ab1024ba99416171af7214fb to your computer and use it in GitHub Desktop.
Axios interceptor for refresh token when you have multiple parallel requests. Demo implementation: https://github.com/Godofbrowser/axios-refresh-multiple-request
// for multiple requests
let isRefreshing = false;
let failedQueue = [];
const processQueue = (error, token = null) => {
failedQueue.forEach(prom => {
if (error) {
prom.reject(error);
} else {
prom.resolve(token);
}
})
failedQueue = [];
}
axios.interceptors.response.use(function (response) {
return response;
}, function (error) {
const originalRequest = error.config;
if (error.response.status === 401 && !originalRequest._retry) {
if (isRefreshing) {
return new Promise(function(resolve, reject) {
failedQueue.push({resolve, reject})
}).then(token => {
originalRequest.headers['Authorization'] = 'Bearer ' + token;
return axios(originalRequest);
}).catch(err => {
return Promise.reject(err);
})
}
originalRequest._retry = true;
isRefreshing = true;
const refreshToken = window.localStorage.getItem('refreshToken');
return new Promise(function (resolve, reject) {
axios.post('http://localhost:8000/auth/refresh', { refreshToken })
.then(({data}) => {
window.localStorage.setItem('token', data.token);
window.localStorage.setItem('refreshToken', data.refreshToken);
axios.defaults.headers.common['Authorization'] = 'Bearer ' + data.token;
originalRequest.headers['Authorization'] = 'Bearer ' + data.token;
processQueue(null, data.token);
resolve(axios(originalRequest));
})
.catch((err) => {
processQueue(err, null);
reject(err);
})
.finally(() => { isRefreshing = false })
})
}
return Promise.reject(error);
});
// Intercept and refresh expired tokens for multiple requests (same implementation but with some abstractions)
//
// HOW TO USE:
// import applyAppTokenRefreshInterceptor from 'axios.refresh_token.2.js';
// import axios from 'axios';
// ...
// applyAppTokenRefreshInterceptor(axios); // register the interceptor with all axios instance
// ...
// - Alternatively:
// const apiClient = axios.create({baseUrl: 'example.com/api'});
// applyAppTokenRefreshInterceptor(apiClient); // register the interceptor with one specific axios instance
// ...
// - With custom options:
// applyAppTokenRefreshInterceptor(apiClient, {
// shouldIntercept: (error) => {
// return error.response.data.errorCode === 'EXPIRED_ACCESS_TOKEN';
// }
// ); // register the interceptor with one specific axios instance
//
// PS: You may need to figure out some minor things yourself as this is just a proof of concept and not a tutorial.
// Forgive me in advance
const shouldIntercept = (error) => {
try {
return error.response.status === 401
} catch (e) {
return false;
}
};
const setTokenData = (tokenData = {}, axiosClient) => {
// If necessary: save to storage
// tokenData's content includes data from handleTokenRefresh(): {
// idToken: data.auth_token,
// refreshToken: data.refresh_token,
// expiresAt: data.expires_in,
// };
};
const handleTokenRefresh = () => {
const refreshToken = window.localStorage.getItem('refreshToken');
return new Promise((resolve, reject) => {
axios.post('http://localhost:8000/auth/refresh', { refreshToken })
.then(({data}) => {
const tokenData = {
idToken: data.auth_token,
refreshToken: data.refresh_token,
expiresAt: data.expires_at,
};
resolve(tokenData);
})
.catch((err) => {
reject(err);
})
});
};
const attachTokenToRequest = (request, token) => {
request.headers['Authorization'] = 'Bearer ' + token;
// If there is an edge case where access token is also set in request query,
// this is also a nice place to add it
// Example: /orders?token=xyz-old-token
if (/\/orders/.test(request.url)) {
request.params.token = token;
}
};
export default (axiosClient, customOptions = {}) => {
let isRefreshing = false;
let failedQueue = [];
const options = {
attachTokenToRequest,
handleTokenRefresh,
setTokenData,
shouldIntercept,
...customOptions,
};
const processQueue = (error, token = null) => {
failedQueue.forEach(prom => {
if (error) {
prom.reject(error);
} else {
prom.resolve(token);
}
});
failedQueue = [];
};
const interceptor = (error) => {
if (!options.shouldIntercept(error)) {
return Promise.reject(error);
}
if (error.config._retry || error.config._queued) {
return Promise.reject(error);
}
const originalRequest = error.config;
if (isRefreshing) {
return new Promise(function (resolve, reject) {
failedQueue.push({resolve, reject})
}).then(token => {
originalRequest._queued = true;
options.attachTokenToRequest(originalRequest, token);
return axiosClient.request(originalRequest);
}).catch(err => {
return Promise.reject(error); // Ignore refresh token request's "err" and return actual "error" for the original request
})
}
originalRequest._retry = true;
isRefreshing = true;
return new Promise((resolve, reject) => {
options.handleTokenRefresh.call(options.handleTokenRefresh)
.then((tokenData) => {
options.setTokenData(tokenData, axiosClient);
options.attachTokenToRequest(originalRequest, tokenData.idToken);
processQueue(null, tokenData.idToken);
resolve(axiosClient.request(originalRequest));
})
.catch((err) => {
processQueue(err, null);
reject(err);
})
.finally(() => {
isRefreshing = false;
})
});
};
axiosClient.interceptors.response.use(undefined, interceptor);
};
let refreshingFunc = undefined;
axios.interceptors.response.use(
(res) => res,
async (error) => {
const originalConfig = error.config;
const token = localStorage.getItem("token");
// if we don't have token in local storage or error is not 401 just return error and break req.
if (!token || !isUnauthorizedError(error)) {
return Promise.reject(error);
}
try {
// the trick here, that `refreshingFunc` is global, e.g. 2 expired requests will get the same function pointer and await same function.
if (!refreshingFunc)
refreshingFunc = renewToken();
const [newToken, newRefreshToken] = await refreshingFunc;
localStorage.setItem("token", newToken);
localStorage.setItem("refreshToken", newRefreshToken);
originalConfig.headers.Authorization = `Bearer ${newToken}`;
// retry original request
try {
return await axios.request(originalConfig);
} catch(innerError) {
// if original req failed with 401 again - it means server returned not valid token for refresh request
if (isUnauthorizedError(innerError)) {
throw innerError;
}
}
} catch (err) {
localStorage.removeItem("token");
localStorage.removeItem("refreshToken");
window.location = `${window.location.origin}/login`;
} finally {
refreshingFunc = undefined;
}
},
)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment