Auto-Refresh CSRF Tokens with Axios Interceptors
This article explains how to implement automatic CSRF (Cross-Site Request Forgery) token refresh logic using Axios interceptors. You will learn how to intercept failed HTTP requests triggered by expired or missing CSRF tokens, fetch a new token from your backend, update authorization headers or cookies, and seamlessly replay the original request without disrupting the user experience.
The CSRF Refresh Workflow
When an application session remains idle, the CSRF token stored on
the client can expire. Subsequent state-changing requests
(POST, PUT, DELETE,
PATCH) will fail, typically with an HTTP
403 Forbidden or 419 Authentication Timeout
status code.
An Axios response interceptor resolves this issue by:
- Catching the CSRF expiration error.
- Requesting a new CSRF token from the server.
- Updating the default headers and the failed request's headers.
- Resending the original request.
- Queuing concurrent requests to avoid redundant token fetches.
Step-by-Step Implementation
1. Create an Axios Instance
Isolate your API logic within a dedicated Axios instance to prevent interceptor collisions with other HTTP calls.
import axios from 'axios';
const apiClient = axios.create({
baseURL: 'https://api.example.com',
withCredentials: true, // Necessary if CSRF tokens depend on cookies/sessions
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest',
},
});
export default apiClient;2. Manage Concurrency and State
If multiple requests fail simultaneously due to an expired token, only one request should fetch a fresh token. Subsequent failed requests should wait in a queue until the new token is available.
let isRefreshing = false;
let failedQueue = [];
const processQueue = (error, token = null) => {
failedQueue.forEach((prom) => {
if (error) {
prom.reject(error);
} else {
prom.resolve(token);
}
});
failedQueue = [];
};3. Implement the Response Interceptor
Attach an interceptor to apiClient.interceptors.response
to capture expired token errors, fetch a replacement, and retry the
request.
// Function to fetch a new token from the server
async function fetchCsrfToken() {
// Using native axios to bypass instance interceptors and avoid loops
const response = await axios.get('https://api.example.com/csrf-token', {
withCredentials: true,
});
return response.data.csrfToken; // Adjust based on your API response structure
}
apiClient.interceptors.response.use(
(response) => response,
async (error) => {
const originalRequest = error.config;
// Check for CSRF failure (adjust status code or error messages to match your backend)
const isCsrfError =
error.response &&
(error.response.status === 419 ||
(error.response.status === 403 &&
error.response.data?.message?.includes('CSRF')));
if (isCsrfError && !originalRequest._retry) {
if (isRefreshing) {
// If a refresh is already in progress, enqueue the request
return new Promise((resolve, reject) => {
failedQueue.push({ resolve, reject });
})
.then((token) => {
originalRequest.headers['X-CSRF-TOKEN'] = token;
return apiClient(originalRequest);
})
.catch((err) => Promise.reject(err));
}
originalRequest._retry = true;
isRefreshing = true;
try {
const newToken = await fetchCsrfToken();
// Update default header for future requests
apiClient.defaults.headers.common['X-CSRF-TOKEN'] = newToken;
// Update the header for the retried request
originalRequest.headers['X-CSRF-TOKEN'] = newToken;
processQueue(null, newToken);
return apiClient(originalRequest);
} catch (refreshError) {
processQueue(refreshError, null);
return Promise.reject(refreshError);
} finally {
isRefreshing = false;
}
}
return Promise.reject(error);
}
);Key Considerations
- Infinite Loop Prevention: Always set a custom flag
such as
originalRequest._retry = truebefore retrying to prevent endless loops if the refresh fails. - Separate Instance for Token Fetching: Use a plain
axioscall (or a separate instance without interceptors) insidefetchCsrfToken()to ensure that a failure to retrieve a CSRF token does not trigger the interceptor again. - Header Matching: Ensure the header key used on the
client (e.g.,
X-CSRF-TOKEN,X-XSRF-TOKEN) matches the header your backend framework expects (such as Laravel, Django, or Express/csurf).