Double Token Auth with Express & Vue: Complete Implementation Guide

This article explains the double token (Access and Refresh) authentication mechanism, detailing its core principles, security benefits, and step‑by‑step implementation with Express on the backend and Vue on the frontend, including token issuance, verification, rotation, middleware, and client‑side handling.

Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Double Token Auth with Express & Vue: Complete Implementation Guide

Core Principles of the Double Token Mechanism

The double token approach uses two tokens with different characteristics to balance security and user experience. An Access Token is short‑lived (e.g., 12 seconds in the demo) and is used to access protected resources. A Refresh Token is long‑lived (e.g., 7 days) and is only used to obtain new Access Tokens.

This design limits the damage of a stolen Access Token while keeping the Refresh Token in a more secure environment, allowing immediate revocation if needed.

Backend Implementation: Express Double Token System

The backend issues and validates tokens, handling configuration, utility functions, middleware, and API endpoints.

Basic Configuration and Dependencies

const express = require('express');</code><code>const cors = require('require('cors');</code><code>const cookieParser = require('cookie-parser');</code><code>const app = express();</code><code>app.use(cors({ origin: ['http://localhost:5173','http://localhost:5174','http://localhost:5175','http://localhost:5176'], credentials: true }));</code><code>app.use(express.json());</code><code>app.use(cookieParser());

This code imports Express and necessary middleware, configures CORS to allow specific front‑end origins with credentials, and enables JSON and cookie parsing.

Token Storage and Core Utility Functions

// In‑memory storage (replace with Redis or a database in production)</code><code>const accessTokens = new Map();</code><code>const refreshTokens = new Map();</code><code>// Get current timestamp (seconds)</code><code>function now() { return Math.floor(Date.now() / 1000); }</code><code>// Generate a random token with a prefix</code><code>function getRandom(prefix) { return `${prefix}-${Math.random().toString(26).slice(2)}${Date.now()}`; }

Tokens are stored in memory for demonstration; production should use a distributed store such as Redis.

Token Issuance Functions

function getAccessToken(userId, ttlSec = 12) { const at = getRandom('AccessToken'); accessTokens.set(at, { userId, expiresIn: now() + ttlSec }); return at; }</code><code>function getRefreshToken(userId, ttlSec = 3600 * 24 * 7) { const rt = getRandom('RefreshToken'); refreshTokens.set(rt, { userId, expiresIn: now() + ttlSec, revoked: false }); return rt; }

These functions create tokens, record the associated user ID, expiration time, and a revocation flag for Refresh Tokens.

Token Verification and Revocation Functions

function verifyAccessToken(at) { const result = accessTokens.get(at); if (!result || result.expiresIn <= now()) return null; return result.userId; }</code><code>function verifyRefreshToken(rt) { const result = refreshTokens.get(rt); if (!result || result.revoked || result.expiresIn <= now()) return null; return result.userId; }</code><code>function revokeRefreshToken(rt) { const result = refreshTokens.get(rt); if (result) result.revoked = true; }

Verification checks existence, expiration, and revocation status, returning the user ID when valid.

Authentication Middleware: First Line of Defense

app.use((req, res, next) => { if (['/auth/login','/auth/refresh','/auth/logout','/login'].includes(req.path)) { return next(); } const token = req.headers.token || ''; const userId = verifyAccessToken(token); if (userId) { req.userId = userId; return next(); } res.send({ status: 401, msg: '未登录或令牌过期' }); });

The middleware bypasses authentication for login‑related routes, validates the Access Token for other requests, attaches the user ID to the request object, and returns a 401 error on failure.

Core API Endpoints

Login Endpoint: Initial Token Issuance

app.post('/auth/login', (req, res) => { const { username } = req.body || {}; const userId = username || 'demoUser'; const at = getAccessToken(userId, 12); const rt = getRefreshToken(userId); res.cookie('rt', rt, { httpOnly: true, sameSite: 'lax', secure: false, path: '/', maxAge: 7 * 24 * 3600 * 1000 }); res.send({ status: 200, data: at }); });

The login route generates both tokens, stores the Refresh Token in an httpOnly cookie, and returns the Access Token to the front‑end.

Refresh Endpoint: Seamless Token Renewal

app.post('/auth/refresh', (req, res) => { const rt = req.cookies.rt; if (!rt) return res.send({ status: 401, msg: '无刷新令牌' }); const userId = verifyRefreshToken(rt); if (!userId) return res.send({ status: 401, msg: '刷新令牌失效' }); revokeRefreshToken(rt); const newRt = getRefreshToken(userId); const newAt = getAccessToken(userId); res.cookie('rt', newRt, { /* same options as login */ }); res.send({ status: 200, data: newAt }); });

This endpoint validates the Refresh Token, revokes the old one, issues new tokens, and updates the cookie, enabling invisible token rotation.

Logout Endpoint: Secure Session Termination

app.post('/auth/logout', (req, res) => { const rt = req.cookies.rt; if (rt) revokeRefreshToken(rt); res.clearCookie('rt', { path: '/' }); res.send({ status: 200, msg: '已登出' }); });

Logout revokes the Refresh Token and clears the cookie, ensuring the session cannot be reused.

Frontend Implementation: Token Management in Vue

The front‑end stores the Access Token, sends it with requests, and automatically refreshes it when expired.

Router Guard: Controlling Page Access

router.beforeEach((to, from, next) => { const token = to.query.token; if (token) { localStorage.setItem('token', token); next({ path: to.path, query: {} }); return; } if (to.meta.requiresAuth) { const currentToken = localStorage.getItem('token'); if (!isValidToken(currentToken)) { window.open(`http://localhost:5174/login?resource=${window.location.origin}${to.path}`); return; } } next(); });

The guard extracts a token from the URL, stores it, and redirects unauthenticated users to the login page.

Axios Interceptors: Automatic Token Handling

// Request interceptor: add token to headers</code><code>request.interceptors.request.use(config => { const token = localStorage.getItem('token'); if (isValidToken(token)) { config.headers = config.headers || {}; config.headers.token = token; } else { localStorage.removeItem('token'); } return config; });</code><code>// Response interceptor: handle 401 and refresh token</code><code>request.interceptors.response.use(async res => { if (res.data && res.data.status === 401) { const original = res.config || {}; if (original._retried) { window.open(`http://localhost:5174/login?resource=${window.location.origin}`); return res; } original._retried = true; if (!isRefreshing) { isRefreshing = true; refreshPromise = request.post('/auth/refresh', {}); } try { const newToken = await refreshPromise; localStorage.setItem('token', newToken); original.headers.token = newToken; return request(original); } catch (e) { window.open(`http://localhost:5174/login?resource=${window.location.origin}`); return res; } finally { isRefreshing = false; } } return res; });

The request interceptor attaches the Access Token, while the response interceptor attempts a token refresh on 401 errors, retrying the original request with the new token.

Login Page Component: Handling Authentication

<script setup> import request from '../server/request'; import { useRoute } from 'vue-router'; import { watch, ref } from 'vue'; const route = useRoute(); const resource = ref(''); const token = localStorage.getItem('token'); function windowPostMessage(token, resource) { if (window.opener) { window.opener.postMessage({ token }, resource); } } watch(() => route.query.resource, (val) => { resource.value = val ? decodeURIComponent(val) : ''; if (token) { windowPostMessage(token, resource.value); } }, { immediate: true }); function login() { request.get('/auth/login').then(res => { const apitoken = res.data.data; localStorage.setItem('token', apitoken); windowPostMessage(apitoken, resource.value); window.location.href = `${resource.value}?token=${apitoken}`; window.close(); }); } </script>

The component obtains a redirect resource, initiates login, stores the Access Token, notifies the opener window, and redirects back with the token.

Double Token Mechanism Demonstration

The following GIF illustrates the complete flow from login, token usage, automatic refresh, to logout.

Double Token Flow GIF
Double Token Flow GIF

Security Considerations of the Double Token Mechanism

Access Token stored in localStorage (XSS risk); Refresh Token stored in httpOnly cookie (protected from XSS).

Use HTTPS in production; set secure flag on cookies.

Short‑lived Access Tokens reduce the window for abuse; Refresh Tokens can be revoked.

Token rotation ensures a stolen Refresh Token can be used only once.

SameSite cookie attribute mitigates CSRF attacks.

Strict verification logic prevents invalid tokens from being accepted.

Conclusion and Extensions

The double token strategy balances security and user experience by combining short‑lived Access Tokens with long‑lived Refresh Tokens. The provided code demonstrates full token lifecycle handling on both server and client sides.

Potential extensions include:

Replacing in‑memory storage with Redis or another distributed store for scalability.

Implementing a token blacklist to handle revoked tokens before expiration.

Adding token revocation notifications for events like password changes.

Using JWT for stateless verification to reduce server load.

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.

Expressaccess tokenRefresh Tokendouble token
Rare Earth Juejin Tech Community
Written by

Rare Earth Juejin Tech Community

Juejin, a tech community that helps developers grow.

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.