JWT Refresh Token — Implement Sliding Sessions with Refresh Tokens
Refresh tokens extend JWT sessions without requiring re-login. Learn how to implement a refresh token flow with short-lived access tokens, secure httpOnly refresh token...
Use the tool
JWT Decoder
Decode and inspect JSON Web Tokens. Local-only — tokens never leave your browser.
Refresh tokens let users stay logged in without storing long-lived credentials in the browser. The pattern: short-lived access tokens (15 minutes) + long-lived refresh tokens (30 days, stored in httpOnly cookies).
Decode and inspect JWT tokens with the JWT Decoder.
The refresh token flow
1. Login → server returns:
- access_token (JWT, 15min expiry, in response body)
- refresh_token (opaque token, 30 day expiry, in httpOnly cookie)
2. Client uses access_token for API requests
3. access_token expires → client calls /auth/refresh endpoint
4. Server validates refresh_token cookie → issues new access_token
(and optionally a new refresh_token — token rotation)
5. Logout → server revokes refresh_token in database
Node.js implementation
import jwt from 'jsonwebtoken';
import crypto from 'crypto';
import { db } from './db.js';
const ACCESS_SECRET = process.env.ACCESS_TOKEN_SECRET;
const REFRESH_SECRET = process.env.REFRESH_TOKEN_SECRET;
const ACCESS_TTL = '15m';
const REFRESH_TTL_DAYS = 30;
// Generate token pair:
function generateTokens(userId) {
const accessToken = jwt.sign(
{ sub: userId, type: 'access' },
ACCESS_SECRET,
{ expiresIn: ACCESS_TTL }
);
const refreshToken = crypto.randomBytes(40).toString('hex'); // Opaque token
return { accessToken, refreshToken };
}
// Login route:
app.post('/auth/login', async (req, res) => {
const { email, password } = req.body;
const user = await authenticateUser(email, password);
if (!user) return res.status(401).json({ error: 'Invalid credentials' });
const { accessToken, refreshToken } = generateTokens(user.id);
// Store refresh token hash in database:
const tokenHash = crypto.createHash('sha256').update(refreshToken).digest('hex');
const expiresAt = new Date(Date.now() + REFRESH_TTL_DAYS * 86400000);
await db.refreshTokens.create({
userId: user.id,
tokenHash,
expiresAt,
createdAt: new Date(),
});
// Set refresh token as httpOnly cookie:
res.cookie('refresh_token', refreshToken, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
maxAge: REFRESH_TTL_DAYS * 86400000,
path: '/auth', // Only sent to /auth routes
});
res.json({ accessToken });
});
Refresh endpoint with token rotation
app.post('/auth/refresh', async (req, res) => {
const refreshToken = req.cookies.refresh_token;
if (!refreshToken) {
return res.status(401).json({ error: 'No refresh token' });
}
// Look up token in database:
const tokenHash = crypto.createHash('sha256').update(refreshToken).digest('hex');
const stored = await db.refreshTokens.findOne({ tokenHash });
if (!stored) {
// Token not found — possible reuse attack:
// Revoke ALL tokens for this user (if we can identify them):
return res.status(401).json({ error: 'Invalid refresh token' });
}
if (stored.expiresAt < new Date()) {
await db.refreshTokens.delete({ id: stored.id });
return res.status(401).json({ error: 'Refresh token expired' });
}
if (stored.revokedAt) {
// Token was already used — possible reuse attack
await db.refreshTokens.deleteAll({ userId: stored.userId });
return res.status(401).json({ error: 'Refresh token reused' });
}
// Token rotation: invalidate old, issue new:
const { accessToken, refreshToken: newRefreshToken } = generateTokens(stored.userId);
// Mark old token as used (don't delete — allows reuse detection):
await db.refreshTokens.update(
{ id: stored.id },
{ revokedAt: new Date() }
);
// Store new refresh token:
const newHash = crypto.createHash('sha256').update(newRefreshToken).digest('hex');
await db.refreshTokens.create({
userId: stored.userId,
tokenHash: newHash,
expiresAt: new Date(Date.now() + REFRESH_TTL_DAYS * 86400000),
});
res.cookie('refresh_token', newRefreshToken, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
maxAge: REFRESH_TTL_DAYS * 86400000,
path: '/auth',
});
res.json({ accessToken });
});
Logout and token revocation
app.post('/auth/logout', async (req, res) => {
const refreshToken = req.cookies.refresh_token;
if (refreshToken) {
const tokenHash = crypto.createHash('sha256').update(refreshToken).digest('hex');
// Revoke the token:
await db.refreshTokens.update(
{ tokenHash },
{ revokedAt: new Date() }
);
}
// Clear the cookie:
res.clearCookie('refresh_token', { path: '/auth' });
res.json({ message: 'Logged out' });
});
// Revoke all sessions (e.g., after password change):
app.post('/auth/logout-all', authenticateMiddleware, async (req, res) => {
await db.refreshTokens.deleteAll({ userId: req.user.id });
res.clearCookie('refresh_token', { path: '/auth' });
res.json({ message: 'All sessions logged out' });
});
Client-side refresh (React/fetch)
let isRefreshing = false;
let refreshQueue = [];
async function authFetch(url, options = {}) {
const token = localStorage.getItem('access_token');
const res = await fetch(url, {
...options,
headers: {
...options.headers,
Authorization: `Bearer ${token}`,
},
credentials: 'include', // Send refresh token cookie
});
if (res.status !== 401) return res;
// Access token expired — refresh:
if (!isRefreshing) {
isRefreshing = true;
const refreshRes = await fetch('/auth/refresh', {
method: 'POST',
credentials: 'include',
});
isRefreshing = false;
if (!refreshRes.ok) {
// Refresh failed — redirect to login
refreshQueue.forEach(reject => reject());
refreshQueue = [];
window.location.href = '/login';
return res;
}
const { accessToken } = await refreshRes.json();
localStorage.setItem('access_token', accessToken);
// Retry queued requests:
refreshQueue.forEach(resolve => resolve(accessToken));
refreshQueue = [];
}
// Retry the original request with new token:
const newToken = localStorage.getItem('access_token');
return fetch(url, {
...options,
headers: {
...options.headers,
Authorization: `Bearer ${newToken}`,
},
credentials: 'include',
});
}
Related tools
- JWT Decoder — inspect JWT token contents
- Hash Generator — generate token hashes
- Password Generator — generate JWT secrets
Related posts
- Decoding a JWT Is Not the Same as Verifying It — Every JWT bug in production reduces to the same mistake: trusting a decoded toke…
- JWT Security Checklist for 2026 — Twelve checks every JWT implementation should pass before shipping. The actual c…
- JWT Authentication Flow — Login, Token Storage, Refresh Tokens — JWT authentication involves issuing tokens on login, sending them with requests,…
- JWT Security Best Practices — Common Vulnerabilities and How to Avoid Them — JWT vulnerabilities include algorithm confusion attacks, weak secrets, missing e…
- JWT Token Structure — Header, Payload, and Signature Explained — A JWT has three Base64URL-encoded parts: header, payload, and signature, separat…
Related tool
JWT Decoder
Decode and inspect JSON Web Tokens. Local-only — tokens never leave your browser.
Written by Mian Ali Khalid. Part of the Encoding & Crypto pillar.