JWT Authentication Flow — Login, Token Storage, Refresh Tokens
JWT authentication involves issuing tokens on login, sending them with requests, and refreshing before expiry. Covers the full flow in Node.js, secure storage (httpOnly cookies...
Use the tool
JWT Decoder
Decode and inspect JSON Web Tokens. Local-only — tokens never leave your browser.
JWT authentication requires issuing tokens on login, validating them on protected routes, refreshing before expiry, and storing them securely. Each step has security implications.
Use the JWT Decoder to inspect any JWT token’s header, payload, and signature.
The complete JWT auth flow
1. User submits credentials (POST /auth/login)
2. Server validates credentials
3. Server issues: access token (short-lived) + refresh token (long-lived)
4. Client stores tokens securely
5. Client sends access token with every request
6. Server validates access token on protected routes
7. When access token expires → use refresh token to get new one
8. Logout: revoke/delete refresh token
Issue tokens on login (Node.js)
import jwt from 'jsonwebtoken';
import crypto from 'crypto';
const ACCESS_TOKEN_SECRET = process.env.JWT_ACCESS_SECRET;
const REFRESH_TOKEN_SECRET = process.env.JWT_REFRESH_SECRET;
function issueAccessToken(userId, role) {
return jwt.sign(
{ sub: userId, role },
ACCESS_TOKEN_SECRET,
{ expiresIn: '15m', issuer: 'api.yourapp.com' }
);
}
function issueRefreshToken(userId) {
return jwt.sign(
{ sub: userId },
REFRESH_TOKEN_SECRET,
{ expiresIn: '7d' }
);
}
app.post('/auth/login', async (req, res) => {
const { email, password } = req.body;
const user = await db.users.findByEmail(email);
if (!user || !(await bcrypt.compare(password, user.passwordHash))) {
return res.status(401).json({ error: 'Invalid credentials' });
}
const accessToken = issueAccessToken(user.id, user.role);
const refreshToken = issueRefreshToken(user.id);
// Store refresh token hash in DB (for revocation):
const tokenHash = crypto.createHash('sha256').update(refreshToken).digest('hex');
await db.refreshTokens.create({ userId: user.id, tokenHash, expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000) });
// Refresh token in httpOnly cookie:
res.cookie('refreshToken', refreshToken, {
httpOnly: true,
secure: true, // HTTPS only
sameSite: 'strict', // CSRF protection
maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days
});
// Access token in response body (client stores in memory):
res.json({ accessToken });
});
Validate tokens on protected routes
function requireAuth(req, res, next) {
const authHeader = req.headers.authorization;
if (!authHeader?.startsWith('Bearer ')) {
return res.status(401).json({ error: 'Missing token' });
}
const token = authHeader.slice(7);
try {
const payload = jwt.verify(token, ACCESS_TOKEN_SECRET, {
issuer: 'api.yourapp.com',
});
req.user = { id: payload.sub, role: payload.role };
next();
} catch (err) {
if (err.name === 'TokenExpiredError') {
return res.status(401).json({ error: 'TOKEN_EXPIRED', message: 'Access token expired' });
}
return res.status(401).json({ error: 'Invalid token' });
}
}
// Usage:
app.get('/api/profile', requireAuth, (req, res) => {
res.json({ userId: req.user.id });
});
Refresh token endpoint
app.post('/auth/refresh', async (req, res) => {
const refreshToken = req.cookies.refreshToken;
if (!refreshToken) {
return res.status(401).json({ error: 'No refresh token' });
}
let payload;
try {
payload = jwt.verify(refreshToken, REFRESH_TOKEN_SECRET);
} catch {
return res.status(401).json({ error: 'Invalid refresh token' });
}
// Verify token exists in DB (not revoked):
const tokenHash = crypto.createHash('sha256').update(refreshToken).digest('hex');
const stored = await db.refreshTokens.findOne({ tokenHash });
if (!stored || stored.expiresAt < new Date()) {
return res.status(401).json({ error: 'Refresh token revoked or expired' });
}
// Refresh token rotation — issue new refresh token, revoke old:
await db.refreshTokens.delete({ tokenHash });
const newAccessToken = issueAccessToken(payload.sub);
const newRefreshToken = issueRefreshToken(payload.sub);
const newHash = crypto.createHash('sha256').update(newRefreshToken).digest('hex');
await db.refreshTokens.create({ userId: payload.sub, tokenHash: newHash });
res.cookie('refreshToken', newRefreshToken, {
httpOnly: true, secure: true, sameSite: 'strict',
maxAge: 7 * 24 * 60 * 60 * 1000,
});
res.json({ accessToken: newAccessToken });
});
Logout
app.post('/auth/logout', async (req, res) => {
const refreshToken = req.cookies.refreshToken;
if (refreshToken) {
const tokenHash = crypto.createHash('sha256').update(refreshToken).digest('hex');
await db.refreshTokens.delete({ tokenHash });
}
res.clearCookie('refreshToken');
res.json({ message: 'Logged out' });
});
Client-side token management
// Store access token in memory (NOT localStorage — XSS vulnerable):
let accessToken = null;
async function login(email, password) {
const res = await fetch('/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password }),
credentials: 'include', // Send/receive cookies
});
const data = await res.json();
accessToken = data.accessToken;
}
async function apiFetch(url, options = {}) {
// If token expired, refresh first:
if (isTokenExpired(accessToken)) {
await refreshTokens();
}
return fetch(url, {
...options,
headers: { ...options.headers, Authorization: `Bearer ${accessToken}` },
credentials: 'include',
});
}
async function refreshTokens() {
const res = await fetch('/auth/refresh', {
method: 'POST',
credentials: 'include',
});
if (!res.ok) {
accessToken = null;
redirectToLogin();
return;
}
const data = await res.json();
accessToken = data.accessToken;
}
function isTokenExpired(token) {
if (!token) return true;
const { exp } = JSON.parse(atob(token.split('.')[1]));
return exp * 1000 < Date.now() + 30000; // 30s buffer
}
Related tools
- JWT Decoder — decode and inspect JWT tokens
- JWT Token Structure — header, payload, signature explained
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 Best Practices — Secure JSON Web Token Implementation — JWT implementation mistakes lead to authentication bypasses and security vulnera…
- 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 Dev Productivity pillar.