React|Optimization & Security

Token Renewal and Black/White Listing via Axios Interceptors

0
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.

  • He has to paste headers: { Authorization: ... } into every API request.
  • If a token expires and throws a 401 error, he needs to wrap every API function in a try-catch block to handle token renewal and retry the request.
  • "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

  • Request Interceptor: Automatically attaches tokens to all requests. Checks a whitelist to avoid sending tokens to external APIs.
  • Response Interceptor: Detects 401 errors and renews the token silently (transparently).
  • Concurrency Control: Uses a Queue and a flag (isRefreshing) to ensure token renewal happens exactly once, even if multiple requests fail simultaneously.
  • UX: Users can continue using the app seamlessly without even realizing their token expired.

  • 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”

    🔗 References

  • Axios Interceptors
  • JWT Authentication Best Practices
  • Comments (0)

    0/1000 characters
    Loading comments...
    Token Renewal and Black/White Listing via Axios Interceptors | VXD Blog