Token Renewal and Black/White Listing via Axios Interceptors

Chris has set up a secure authentication structure as learned in the previous post. However, as he starts writing code, he finds more than a few annoyances.
"Doing this manually makes no sense. Can't the system automatically attach the token when I call an API, and fix it and resend it if an error occurs?"
For lazy (but smart) developers like Chris, Axios Interceptors exist. Today, we will implement automated authentication logic using Interceptors, the gatekeepers of HTTP requests and responses.
1. Request Interceptor: Auto-Injection and Whitelisting
The Request Interceptor runs "right before the API request departs." Here, we plant the access token into the header.
A crucial point here is handling the Whitelist. There is no need to send my precious token to external services like S3 image uploads or map APIs that aren't my server. (In fact, it's a security risk.)
// axiosInstance.ts
import axios from 'axios';
export const api = axios.create({
baseURL: 'https://api.my-service.com',
timeout: 5000,
});
// List of URLs that do not require a token (Whitelist)
const WHITELIST = ['/auth/login', '/auth/refresh', '/public/health'];
api.interceptors.request.use(
(config) => {
// 1. If it's a whitelisted URL, pass through without adding headers
if (WHITELIST.includes(config.url || '')) {
return config;
}
// 2. Retrieve token from memory (variable/store)
const token = useAuthStore.getState().accessToken;
// 3. Inject into header if token exists
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => Promise.reject(error)
);2. Response Interceptor: 401 Handling and Retrying
The Response Interceptor runs "right after the server's response arrives, but before it reaches the component." Here, we intercept 401 Unauthorized errors and renew the token.
Solving Concurrency Issues (Queueing)
What if Chris enters the dashboard, causing 5 API calls to fire simultaneously while the token is expired?
All 5 requests will fail, triggering 5 separate token refresh requests (/refresh). This wastes server resources and might even cause a logout due to RTR (Refresh Token Rotation) policy violations.
Therefore, we need logic that says, "If renewal is already in progress, get in line."
// axiosInstance.ts (continued)
let isRefreshing = false;
let failedQueue: any[] = [];
const processQueue = (error: any, token: string | null = null) => {
failedQueue.forEach((prom) => {
if (error) {
prom.reject(error);
} else {
prom.resolve(token);
}
});
failedQueue = [];
};
api.interceptors.response.use(
(response) => response,
async (error) => {
const originalRequest = error.config;
// 1. If it's a 401 error and hasn't been retried yet
if (error.response?.status === 401 && !originalRequest._retry) {
// 2. If someone is already refreshing? Queue up and wait
if (isRefreshing) {
return new Promise((resolve, reject) => {
failedQueue.push({ resolve, reject });
})
.then((token) => {
originalRequest.headers.Authorization = `Bearer ${token}`;
return api(originalRequest);
})
.catch((err) => Promise.reject(err));
}
// 3. I'll take the lead and start refreshing
originalRequest._retry = true;
isRefreshing = true;
try {
// Token refresh request (Cookie containing Refresh Token is sent automatically)
const { data } = await axios.post('https://api.my-service.com/auth/refresh');
// Save new token
const newToken = data.accessToken;
useAuthStore.getState().setAccessToken(newToken);
// Replace header
originalRequest.headers.Authorization = `Bearer ${newToken}`;
// Process waiting requests
processQueue(null, newToken);
// Retry my failed request
return api(originalRequest);
} catch (err) {
// Renewal failed (Refresh Token expired too) -> Force Logout
processQueue(err, null);
useAuthStore.getState().logout();
window.location.href = '/login'; // Or redirect
return Promise.reject(err);
} finally {
isRefreshing = false;
}
}
return Promise.reject(error);
}
);3. Blacklisting? (Logout Handling)
In frontend terms, "blacklist management" usually refers to logout handling.
While the frontend can simply delete the access token, the server still sees that token as "valid" until its expiration time.
For perfect security, when calling the logout API, the server should register that access token in a Blacklist (e.g., in Redis) to prevent further use. The frontend simply needs to remember to call this API when the logout button is clicked.
const logout = async () => {
try {
await api.post('/auth/logout'); // Request server to blacklist token
} finally {
useAuthStore.getState().setAccessToken(null); // Delete from client memory
}
};Key Takeaways
This concludes Part 4: Performance Optimization and Security.
We now know how to make React apps fast and secure. The final gateway is the design ability to create "Sustainable Code." How should we assemble dozens of components to maximize reusability?
Part 5: Architecture and Design begins with the first topic: “Maximizing Reusability: Compound Component Pattern and Headless UI”