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.
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.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
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.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
