How to Achieve a Seamless “Never‑Logout” Experience with Token Refresh

This article explains why short‑lived access tokens cause user interruptions, introduces the dual‑token authentication model with refresh tokens, and provides a complete Axios interceptor implementation that transparently renews tokens, handles concurrency, and gracefully logs out when refresh fails.

JavaScript
JavaScript
JavaScript
How to Achieve a Seamless “Never‑Logout” Experience with Token Refresh

Root cause: Access Token’s inherent paradox

Access tokens are deliberately short‑lived (e.g., 30 minutes to 1 hour) to limit the damage of a leak, but users dislike being forced to re‑login frequently, creating a tension between security and user experience.

Core idea: Dual‑Token Authentication System

The solution is to use two tokens:

Access Token (access token)

Purpose: Sent in the Authorization header to access protected APIs.

Characteristics: Short lifespan (≈1 hour), stateless, stored in client memory (e.g., Vuex/Redux).

Refresh Token (refresh token)

Purpose: Used to obtain a new access token when the current one expires.

Characteristics: Long lifespan (7–30 days), stateful, must be stored securely (commonly in an HttpOnly cookie).

Because the access token is stateless, it cannot be revoked, whereas the refresh token is stateful and can be invalidated via a server‑side whitelist or revocation list when a user changes password or logs out.

Detailed Refresh Workflow (Invisible to Users)

Initial login: Server returns both an access token and a refresh token.

Normal request: Client includes the access token in the Authorization header.

Token expiration: API returns 401 Unauthorized when the access token is expired.

Intercept 401: An Axios interceptor pauses the failed request instead of showing an error.

Refresh request: The interceptor calls a dedicated refresh endpoint (e.g., /api/auth/refresh) using the refresh token.

Handle refresh result:

Success – server validates the refresh token, issues a new access token (optionally a new refresh token), and the client retries the original request.

Failure – if the refresh token is also invalid, the client clears authentication data, logs out the user, and redirects to the login page.

Practical Example: Axios Interceptor for Seamless Refresh

Below is a complete implementation that also handles concurrent 401 responses.

import axios from 'axios';

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

// Request interceptor – attach access token
service.interceptors.request.use(config => {
  const accessToken = getAccessTokenFromStore();
  if (accessToken) {
    config.headers['Authorization'] = `Bearer ${accessToken}`;
  }
  return config;
}, error => Promise.reject(error));

import { refreshTokenApi } from './auth';
let isRefreshing = false; // flag to avoid duplicate refreshes
let requests = []; // queue for pending requests

service.interceptors.response.use(response => response, async error => {
  const { config, response: { status } } = error;
  if (status !== 401) return Promise.reject(error);

  if (isRefreshing) {
    return new Promise(resolve => {
      requests.push(() => resolve(service(config)));
    });
  }

  isRefreshing = true;
  try {
    const { newAccessToken } = await refreshTokenApi(); // refresh token sent via HttpOnly cookie
    setAccessTokenInStore(newAccessToken);
    config.headers['Authorization'] = `Bearer ${newAccessToken}`;
    requests.forEach(cb => cb());
    requests = [];
    return service(config);
  } catch (refreshError) {
    console.error('Unable to refresh token.', refreshError);
    logoutUser(); // clear tokens and redirect to login
    return Promise.reject(refreshError);
  } finally {
    isRefreshing = false;
  }
});

export default service;

The code uses isRefreshing and a requests queue to ensure only one refresh request runs at a time; subsequent 401s wait for the first refresh to finish before retrying.

Key Takeaways

Concurrent handling prevents redundant refresh calls.

The lock‑like mechanism makes the refresh operation atomic, avoiding race conditions.

When the refresh token also expires, the system performs an elegant logout, preserving security.

Implementing an invisible token‑refresh mechanism is now a standard practice for modern web applications, balancing authentication complexity, security, and seamless 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.

frontendSecurityAuthenticationaxiosrefresh()Token
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.