Security
JWT Security
JSON Web Tokens for secure authentication
Intermediate
security auth tokens jwt
Definition
JSON Web Token (JWT) is an open standard for securely transmitting information between parties as a JSON object. JWTs are digitally signed and optionally encrypted, making them ideal for authentication and information exchange.
JWT Structure
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ
.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
Header.Payload.Signature
Header
{
"alg": "HS256",
"typ": "JWT"
}
Payload (Claims)
{
"sub": "1234567890",
"name": "John Doe",
"iat": 1516239022,
"exp": 1516242622,
"role": "admin"
}
Signature
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret
)
Client-Side Usage
Storing Tokens
// LocalStorage (accessible to JavaScript - XSS risk)
localStorage.setItem('token', jwt);
// SessionStorage (cleared when tab closes)
sessionStorage.setItem('token', jwt);
// Cookie with HttpOnly (secure, server only - recommended)
// Set by server: Set-Cookie: token=xxx; HttpOnly; Secure; SameSite=Strict
Sending Tokens
// As Authorization header
fetch('/api/protected', {
headers: {
'Authorization': `Bearer ${localStorage.getItem('token')}`
}
});
// Axios interceptor
axios.interceptors.request.use(config => {
const token = localStorage.getItem('token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
Token Parsing
// Decode JWT (no verification - client-side only)
function parseJWT(token) {
try {
const base64Url = token.split('.')[1];
const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
const jsonPayload = decodeURIComponent(
atob(base64)
.split('')
.map(c => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2))
.join('')
);
return JSON.parse(jsonPayload);
} catch (e) {
return null;
}
}
// Check expiration
const payload = parseJWT(token);
if (payload?.exp < Date.now() / 1000) {
// Token expired, refresh or logout
}
Security Best Practices
1. Short Expiration + Refresh Tokens
// Access token: short-lived (15 minutes)
// Refresh token: long-lived, stored securely
async function getValidToken() {
let token = localStorage.getItem('accessToken');
const payload = parseJWT(token);
// If expiring in less than 5 minutes
if (payload?.exp - Date.now() / 1000 < 300) {
const refreshToken = localStorage.getItem('refreshToken');
const response = await fetch('/api/refresh', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ refreshToken })
});
if (response.ok) {
const { accessToken } = await response.json();
localStorage.setItem('accessToken', accessToken);
return accessToken;
} else {
// Refresh failed, logout
logout();
}
}
return token;
}
2. Token Rotation
// Each refresh gives new refresh token
// Old refresh tokens invalidated (prevents replay attacks)
async function refreshToken() {
const response = await fetch('/api/refresh', {
method: 'POST',
credentials: 'include' // Send refresh token cookie
});
if (response.ok) {
const { accessToken } = await response.json();
localStorage.setItem('accessToken', accessToken);
}
}
3. Secure Storage
// Bad: localStorage (vulnerable to XSS)
localStorage.setItem('token', token);
// Good: httpOnly cookie (XSS can't steal)
// Set by server, not accessible to JS
// Better: Memory-only with silent refresh
let accessToken = null;
async function getToken() {
if (!accessToken) {
// Try silent refresh
accessToken = await silentRefresh();
}
return accessToken;
}
Common Vulnerabilities
None Algorithm Attack
// Attacker modifies header to use "none" algorithm
// Always verify algorithm on server
Weak Secrets
// Bad: Short or predictable secrets
const secret = 'password123';
// Good: Long random strings
const secret = crypto.randomBytes(64).toString('hex');
Token Theft
// XSS steals token from localStorage
// Mitigation: httpOnly cookies + CSP
// Implement logout on all devices
function logoutAll() {
fetch('/api/logout-all', {
method: 'POST',
credentials: 'include'
});
localStorage.removeItem('token');
}
Implementation Example
// auth.js
class AuthManager {
constructor() {
this.accessToken = null;
this.refreshPromise = null;
}
async getToken() {
if (this.accessToken && !this.isExpired(this.accessToken)) {
return this.accessToken;
}
// Prevent multiple simultaneous refresh requests
if (!this.refreshPromise) {
this.refreshPromise = this.refresh().finally(() => {
this.refreshPromise = null;
});
}
return this.refreshPromise;
}
async refresh() {
try {
const response = await fetch('/api/refresh', {
method: 'POST',
credentials: 'include'
});
if (!response.ok) throw new Error('Refresh failed');
const { accessToken } = await response.json();
this.accessToken = accessToken;
return accessToken;
} catch (error) {
this.logout();
throw error;
}
}
isExpired(token) {
const payload = parseJWT(token);
return payload?.exp < Date.now() / 1000;
}
logout() {
this.accessToken = null;
fetch('/api/logout', { method: 'POST', credentials: 'include' });
window.location = '/login';
}
}
export const auth = new AuthManager();
Key Takeaway
JWTs enable stateless authentication but require careful handling. Use short-lived access tokens with refresh tokens, store tokens securely (prefer httpOnly cookies), implement token rotation, and always validate on the server. Never trust client-side token parsing for security decisions.