Fading Coder

One Final Commit for the Last Sprint

Home > Tech > Content

Implementing Silent Token Refresh in Vue and Node.js Applications

Tech 1

User experience suffers when an applciation forces a logout due to an expired authentication token. Silent refresh addresses this by transparently renewing tokens in the background.

Token Refresh Strategies

Redis-based Token Extension

A common backend-driven approach stores tokens in Redis with a confiugrable Time-To-Live (TTL). Each valid request resets the token's expiry, maintaining a single active token. The client remains unaware of the renewal process.

Dual-Token Archietcture

This client-managed strategy employs two tokens: a short-lived Access Token and a longer-lived Refresh Token.

Mechanism

  1. The client authenticates with credentials, receiving both tokens upon success.
  2. The Access Token is attached to API requests. The backend validates it.
  3. If the Access Token is invalid, the client uses the Refresh Token to request a new pair.
  4. The backend validates the Refresh Token. If valid, new tokens are issued. If invalid, the user must re-authenticate.
  5. The client stores the new tokens and retries the original request.

Implementation Example

Backend (Node.js with Koa)

Token generation utility (tokenManager.js):

const jwt = require('jsonwebtoken');
const SECRET_KEY = 'your_secure_secret_key';
const ACCESS_EXPIRY = 5; // Short expiry for access token
const REFRESH_EXPIRY = 15; // Longer expiry for refresh token

const generateAccessToken = (userData = {}) => {
    return jwt.sign(userData, SECRET_KEY, { expiresIn: ACCESS_EXPIRY });
};
const generateRefreshToken = (userData = {}) => {
    return jwt.sign(userData, SECRET_KEY, { expiresIn: REFRESH_EXPIRY });
};
module.exports = { SECRET_KEY, generateAccessToken, generateRefreshToken };

Middleware for token validation (authMiddleware.js):

const { SECRET_KEY } = require('./tokenManager');
const jwt = require('jsonwebtoken');

const publicPaths = ['/authenticate', '/renew-token'];
const isPublicPath = (url) => publicPaths.includes(url);

const validateAccessToken = async (ctx, next) => {
    if (isPublicPath(ctx.path)) return await next();

    const token = ctx.request.headers['authorization'];
    if (!token) {
        ctx.status = 401;
        ctx.body = { error: 'No token provided' };
        return;
    }
    try {
        jwt.verify(token, SECRET_KEY);
        await next();
    } catch (err) {
        ctx.status = 401;
        ctx.body = { error: 'Invalid or expired token' };
    }
};
module.exports = validateAccessToken;

Frontend (Vue 3 with Axios)

Storage helper (authStorage.js):

const ACCESS_KEY = 'app_access_token';
const REFRESH_KEY = 'app_refresh_token';

export const storeTokens = (access, refresh) => {
    localStorage.setItem(ACCESS_KEY, access);
    localStorage.setItem(REFRESH_KEY, refresh);
};
export const getAccessToken = () => localStorage.getItem(ACCESS_KEY);
export const getRefreshToken = () => localStorage.getItem(REFRESH_KEY);
export const clearTokens = () => {
    localStorage.removeItem(ACCESS_KEY);
    localStorage.removeItem(REFRESH_KEY);
};

Token refresh orchestration (refreshHandler.js):

import { getRefreshToken, clearTokens, storeTokens } from './authStorage';
import apiClient from './apiClient';

let pendingRequests = [];
let isRefreshing = false;

export const queueRequest = (request) => {
    pendingRequests.push(request);
};
export const replayRequests = () => {
    pendingRequests.forEach(callback => callback());
    pendingRequests = [];
};
export const refreshAccessToken = () => {
    if (isRefreshing) return;
    isRefreshing = true;

    const refreshToken = getRefreshToken();
    if (!refreshToken) {
        clearTokens();
        isRefreshing = false;
        return;
    }
    apiClient.get('/renew-token', {
        headers: { 'X-Refresh-Token': refreshToken }
    }).then(response => {
        if (response.data.accessToken) {
            storeTokens(response.data.accessToken, response.data.refreshToken);
            replayRequests();
        }
        isRefreshing = false;
    }).catch(() => {
        clearTokens();
        isRefreshing = false;
    });
};

Configured HTTP client (apiClient.js):

import axios from 'axios';
import { getAccessToken } from './authStorage';
import { queueRequest, refreshAccessToken } from './refreshHandler';

const client = axios.create({
    baseURL: 'http://localhost:3000/api',
    timeout: 10000
});

client.interceptors.request.use(config => {
    const token = getAccessToken();
    if (token) config.headers.Authorization = token;
    return config;
});

client.interceptors.response.use(
    response => response.data,
    error => {
        const { config, response } = error;
        if (response?.status === 401 && response.data?.error === 'Invalid or expired token') {
            return new Promise(resolve => {
                queueRequest(() => resolve(client(config)));
                refreshAccessToken();
            });
        }
        return Promise.reject(error);
    }
);
export default client;

Vue Component Usage (App.vue):

<script setup>
import { loginUser, fetchProtectedData } from './services/api';
import { storeTokens } from './utils/authStorage';

const handleLogin = async () => {
    const result = await loginUser({ username: 'user', password: 'pass' });
    storeTokens(result.accessToken, result.refreshToken);
};
const handleRequest = async () => {
    const data = await fetchProtectedData();
    console.log('Data received:', data);
};
</script>

<template>
    <button @click="handleLogin">Authenticate</button>
    <button @click="handleRequest">Fetch Data</button>
</template>

Related Articles

Understanding Strong and Weak References in Java

Strong References Strong reference are the most prevalent type of object referencing in Java. When an object has a strong reference pointing to it, the garbage collector will not reclaim its memory. F...

Comprehensive Guide to SSTI Explained with Payload Bypass Techniques

Introduction Server-Side Template Injection (SSTI) is a vulnerability in web applications where user input is improper handled within the template engine and executed on the server. This exploit can r...

Implement Image Upload Functionality for Django Integrated TinyMCE Editor

Django’s Admin panel is highly user-friendly, and pairing it with TinyMCE, an effective rich text editor, simplifies content management significantly. Combining the two is particular useful for bloggi...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.