JWT Access and Refresh Token Flow
JSON Web Tokens (JWT) are a compact, URL-safe means of representing claims between two parties. In an Express API, JWTs are the dominant mechanism for stateless authentication: the server issues a signed token after login, the client stores it and attaches it to subsequent requests, and the server verifies the signature without consulting a database. The access/refresh token pattern extends this by issuing two tokens — a short-lived access token and a long-lived refresh token — to balance security and user experience.
JWT Structure and Signing
A JWT consists of three base64url-encoded parts separated by dots: Header.Payload.Signature. The header identifies the algorithm (HS256, RS256). The payload carries claims: iss (issuer), sub (subject/user ID), exp (expiry), iat (issued at), and any custom claims you add. The signature is a HMAC or RSA hash of the header and payload using your secret key. Verification re-computes the hash and compares it, confirming the token has not been tampered with.
npm install jsonwebtoken// utils/jwt.js
const jwt = require('jsonwebtoken');
const ACCESS_SECRET = process.env.JWT_ACCESS_SECRET;
const REFRESH_SECRET = process.env.JWT_REFRESH_SECRET;
function signAccess(payload) {
return jwt.sign(payload, ACCESS_SECRET, { expiresIn: '15m' });
}
function signRefresh(payload) {
return jwt.sign(payload, REFRESH_SECRET, { expiresIn: '7d' });
}
function verifyAccess(token) {
return jwt.verify(token, ACCESS_SECRET); // throws on invalid/expired
}
function verifyRefresh(token) {
return jwt.verify(token, REFRESH_SECRET);
}
module.exports = { signAccess, signRefresh, verifyAccess, verifyRefresh };Login and Token Issuance
The login endpoint verifies credentials, then issues both an access token (returned in the response body) and a refresh token (stored in an HttpOnly cookie or returned in the body for mobile clients). HttpOnly cookies prevent JavaScript from reading the refresh token, reducing XSS risk. The access token is sent as a Bearer token in the Authorization header on subsequent requests.
// routes/auth.js
const router = require('express').Router();
const bcrypt = require('bcrypt');
const User = require('../models/User');
const { signAccess, signRefresh } = require('../utils/jwt');
router.post('/login', async (req, res) => {
const { email, password } = req.body;
const user = await User.findOne({ email });
if (!user || !(await bcrypt.compare(password, user.passwordHash))) {
return res.status(401).json({ error: 'Invalid credentials' });
}
const payload = { sub: user.id, role: user.role };
const accessToken = signAccess(payload);
const refreshToken = signRefresh({ sub: user.id });
// Store hashed refresh token in DB for revocation
await User.updateOne({ _id: user.id }, { refreshTokenHash: await bcrypt.hash(refreshToken, 10) });
res.cookie('refreshToken', refreshToken, {
httpOnly: true, secure: true, sameSite: 'strict', maxAge: 7 * 24 * 60 * 60 * 1000
});
res.json({ accessToken });
});