How to Implement Seamless Token Refresh with Axios for a Never‑Expired User Experience

This article explains how to create a seamless, never‑expired user experience by using a dual‑token authentication system with Access and Refresh Tokens, detailing the workflow and providing a complete Axios interceptor implementation to handle token refresh automatically without disrupting users.

JavaScript
JavaScript
JavaScript
How to Implement Seamless Token Refresh with Axios for a Never‑Expired User Experience

Nothing is more frustrating than being interrupted with a "login expired, please log in again" message while the user is actively working, which not only ruins the experience but can cause unsaved data loss.

Root Cause: The Inherent Paradox of Access Tokens

We need to understand why tokens must be refreshed. Access Tokens are used to authenticate every API request, and for security their lifespan is kept short (e.g., 30 minutes or 1 hour). A longer lifespan would increase the risk if the token is leaked.

This creates a contradiction:

Security requirement: Access Token must have a short validity.

User‑experience requirement: Users do not want to be forced to log in repeatedly.

To resolve this, the Refresh Token was introduced.

Core Idea: Dual‑Token Authentication System

The invisible refresh mechanism relies on two types of tokens:

Access Token (access token)

Purpose: Used to access protected API resources, placed in the request Header.

Characteristics: Short lifespan (e.g., 1 hour), stateless, server does not store it.

Storage: Usually kept in client memory (e.g., Vuex/Redux) for frequent access.

Refresh Token (refresh token)

Purpose: Used to obtain a new Access Token when the current one expires.

Characteristics: Long lifespan (e.g., 7 days or 30 days), bound to a specific user, server must securely store its validity.

Storage: Should be stored in an HttpOnly cookie to prevent JavaScript access (e.g., XSS).

Why not use only a Refresh Token? Access Tokens are typically stateless, so they cannot be actively revoked, whereas Refresh Tokens are stateful; the server maintains a whitelist or revocation list to invalidate them when a user changes password or logs out.

Detailed Workflow of Invisible Refresh

The following steps describe how the "magic" works:

Initial login: User logs in with credentials, server returns an Access Token and a Refresh Token.

Normal request: Client stores the Access Token and sends it in the Authorization header for each API call.

Token expiration: When the Access Token expires, the server responds with 401 Unauthorized.

Intercept 401: The client’s request layer (e.g., Axios interceptor) catches the 401, pauses the failed request, and does not immediately notify the user.

Refresh request: The interceptor uses the Refresh Token to call a dedicated refresh endpoint (e.g., /api/auth/refresh).

Handle refresh result:

Success: Server validates the Refresh Token, issues a new Access Token (optionally a new Refresh Token – “refresh token rotation”), and returns them.

Failure: If the Refresh Token is also expired or invalid, the server returns an error such as 403 Forbidden, ending the session.

Retry or terminate:

If refresh succeeds, the client automatically retries the original request with the new Access Token, making the user unaware of any interruption.

If refresh fails, the client clears all authentication data, forces logout, and redirects to the login page.

Practical Exercise: Implementing Invisible Refresh with Axios Interceptor

Axios interceptors are ideal for this flow. Below is a complete implementation that also handles concurrent requests.

1. Create an Axios instance

// a-pi/request.js
import axios from 'axios';

const service = axios.create({
  baseURL: '/api',
  timeout: 10000,
});

service.interceptors.request.use(
  config => {
    // Retrieve token from state management (e.g., Vuex/Pinia/Redux)
    const accessToken = getAccessTokenFromStore();
    if (accessToken) {
      config.headers['Authorization'] = `Bearer ${accessToken}`;
    }
    return config;
  },
  error => Promise.reject(error)
);

2. Core: Response interceptor

This is the key part that handles token refresh.

// a-pi/request.js (continued)
import { refreshTokenApi } from './auth';

let isRefreshing = false; // flag to control refresh state
let requests = []; // queue for requests paused due to token expiration

service.interceptors.response.use(
  response => response,
  async error => {
    const { config, response: { status } } = error;

    // 1. If not a 401 error, reject directly
    if (status !== 401) {
      return Promise.reject(error);
    }

    // 2. If a refresh is already in progress, queue the request
    if (isRefreshing) {
      return new Promise(resolve => {
        requests.push(() => resolve(service(config)));
      });
    }

    isRefreshing = true;

    try {
      // 3. Call the refresh token API (Refresh Token sent automatically via HttpOnly cookie)
      const { newAccessToken } = await refreshTokenApi();

      // 4. Update local storage of the access token
      setAccessTokenInStore(newAccessToken);

      // 5. Retry the original request with the new token
      config.headers['Authorization'] = `Bearer ${newAccessToken}`;

      // 6. Replay all queued requests
      requests.forEach(cb => cb());
      requests = [];

      return service(config);
    } catch (refreshError) {
      // 7. If refresh also fails, perform logout
      console.error('Unable to refresh token.', refreshError);
      logoutUser(); // clear tokens and redirect to login
      return Promise.reject(refreshError);
    } finally {
      isRefreshing = false;
    }
  }
);

export default service;

Code analysis:

Concurrent handling: The isRefreshing flag and requests queue ensure that only one refresh request is sent while subsequent 401 errors wait.

Atomic operation: This “lock” mechanism makes the token refresh process atomic, avoiding race conditions.

Graceful degradation: When the Refresh Token also becomes invalid, logoutUser() clears credentials and redirects the user to re‑authenticate.

Invisible token refresh is now a standard feature for modern web applications, hiding authentication complexity from users and providing a smooth, uninterrupted experience.

Implementing this mechanism requires not only a few lines of code but also a deep understanding of the balance between authentication flow, security, and user experience.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

backendfrontendAxiosRefreshToken
JavaScript
Written by

JavaScript

Provides JavaScript enthusiasts with tutorials and experience sharing on web front‑end technologies, including JavaScript, Node.js, Deno, Vue.js, React, Angular, HTML5, CSS3, and more.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.