What is JWT?
JWT (JSON Web Token) is a compact, self-contained, stateless authentication mechanism. The token contains all the information the server needs - no session lookups required.
JWT Structure:
┌─────────────┬──────────────────────┬──────────────────────┐
│ Header │ Payload │ Signature │
│ { │ { │ HMACSHA256( │
│ "alg": │ "userId": 1, │ base64(header) + │
│ "HS256" │ "role": "admin", │ "." + │
│ } │ "iat": 1689000000, │ base64(payload), │
│ │ "exp": 1689086400 │ secret │
│ │ } │ ) │
└─────────────┴──────────────────────┴──────────────────────┘
│ │ │
base64url base64url signed hash
Stateless: The server doesn’t store the token - it just verifies the signature.
Browser Server
│ │
│── POST /login ────────────────→│
│ { username, password } │ Verify credentials
│ │ Create JWT { userId, role }
│←── { token: "eyJhbG..." } ────│ Sign with secret
│ │
│── GET /api/users ─────────────→│
│ Authorization: Bearer eyJ... │ Verify signature
│ │ Decode payload → userId
│←── 200 JSON data ──────────────│
Installation
npm install jsonwebtoken bcrypt
jsonwebtoken- create and verify JWTsbcrypt- hash passwords before storing
Creating a JWT
const jwt = require('jsonwebtoken');
const payload = {
userId: user._id,
username: user.username,
role: user.role
};
const secret = process.env.JWT_SECRET || 'fallback-secret';
const token = jwt.sign(payload, secret, {
expiresIn: '24h' // Token expires in 24 hours
});
Payload Best Practices
// ✅ Good - minimal, essential data
const payload = {
sub: user._id, // Subject (standard claim)
role: user.role, // For authorization
iss: 'myapp', // Issuer (standard claim)
iat: Date.now() // Issued at (auto-added)
};
// ❌ Bad - sensitive data
const payload = {
password: user.password, // Never include secrets!
ssn: '123-45-6789', // Never include PII!
creditCard: '4111...' // Never include financial data!
};
Token Expiration Options
// Short-lived access token
const accessToken = jwt.sign(payload, secret, { expiresIn: '15m' });
// Long-lived refresh token
const refreshToken = jwt.sign(payload, refreshSecret, { expiresIn: '7d' });
// Numeric expiration (in seconds)
const token = jwt.sign(payload, secret, { expiresIn: 3600 });
// Custom expiration date
const token = jwt.sign(payload, secret, { expiresIn: '30 days' });
Verifying a JWT
function verifyToken(token) {
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
return { valid: true, decoded };
} catch (err) {
if (err.name === 'TokenExpiredError') {
return { valid: false, error: 'Token expired' };
}
if (err.name === 'JsonWebTokenError') {
return { valid: false, error: 'Invalid token' };
}
return { valid: false, error: 'Authentication failed' };
}
}
Auth Middleware
// middleware/auth.js
function authenticate(req, res, next) {
const authHeader = req.headers['authorization'];
if (!authHeader) {
return res.status(401).json({ error: 'No authorization header' });
}
// Format: "Bearer <token>"
const token = authHeader.startsWith('Bearer ')
? authHeader.slice(7)
: authHeader;
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.user = decoded; // Attach user data to request
next();
} catch (err) {
if (err.name === 'TokenExpiredError') {
return res.status(401).json({ error: 'Token expired', code: 'TOKEN_EXPIRED' });
}
return res.status(403).json({ error: 'Invalid token' });
}
}
function authorize(...allowedRoles) {
return (req, res, next) => {
if (!req.user) {
return res.status(401).json({ error: 'Not authenticated' });
}
if (!allowedRoles.includes(req.user.role)) {
return res.status(403).json({ error: 'Insufficient permissions' });
}
next();
};
}
module.exports = { authenticate, authorize };
Complete Auth Flow
const express = require('express');
const jwt = require('jsonwebtoken');
const bcrypt = require('bcrypt');
const { authenticate, authorize } = require('./middleware/auth');
const app = express();
app.use(express.json());
// Mock user store
const users = [];
// Register
app.post('/register', async (req, res) => {
const { username, password } = req.body;
if (users.find(u => u.username === username)) {
return res.status(409).json({ error: 'Username already exists' });
}
const hashedPassword = await bcrypt.hash(password, 10);
const user = { id: users.length + 1, username, password: hashedPassword, role: 'user' };
users.push(user);
res.status(201).json({ message: 'User created' });
});
// Login
app.post('/login', async (req, res) => {
const { username, password } = req.body;
const user = users.find(u => u.username === username);
if (!user || !(await bcrypt.compare(password, user.password))) {
return res.status(401).json({ error: 'Invalid credentials' });
}
const token = jwt.sign(
{ sub: user.id, username: user.username, role: user.role },
process.env.JWT_SECRET,
{ expiresIn: '24h' }
);
res.json({
token,
user: { id: user.id, username: user.username, role: user.role }
});
});
// Protected routes
app.get('/profile', authenticate, (req, res) => {
res.json({ user: req.user });
});
app.get('/admin', authenticate, authorize('admin'), (req, res) => {
res.json({ secret: 'Admin dashboard data' });
});
app.get('/moderator', authenticate, authorize('admin', 'moderator'), (req, res) => {
res.json({ secret: 'Moderator panel' });
});
app.listen(3000);
Refresh Token Pattern
const refreshTokens = []; // Use Redis/DB in production
app.post('/login', async (req, res) => {
const { username, password } = req.body;
const user = users.find(u => u.username === username);
if (!user || !(await bcrypt.compare(password, user.password))) {
return res.status(401).json({ error: 'Invalid credentials' });
}
const accessToken = jwt.sign(
{ sub: user.id, role: user.role },
process.env.JWT_SECRET,
{ expiresIn: '15m' }
);
const refreshToken = jwt.sign(
{ sub: user.id, type: 'refresh' },
process.env.JWT_REFRESH_SECRET,
{ expiresIn: '7d' }
);
refreshTokens.push(refreshToken);
res.json({ accessToken, refreshToken });
});
// Refresh endpoint
app.post('/token/refresh', (req, res) => {
const { refreshToken } = req.body;
if (!refreshToken) {
return res.status(401).json({ error: 'Refresh token required' });
}
if (!refreshTokens.includes(refreshToken)) {
return res.status(403).json({ error: 'Invalid refresh token' });
}
try {
const decoded = jwt.verify(refreshToken, process.env.JWT_REFRESH_SECRET);
const newAccessToken = jwt.sign(
{ sub: decoded.sub, role: decoded.role },
process.env.JWT_SECRET,
{ expiresIn: '15m' }
);
res.json({ accessToken: newAccessToken });
} catch (err) {
return res.status(403).json({ error: 'Invalid refresh token' });
}
});
// Logout - invalidate refresh token
app.post('/logout', (req, res) => {
const { refreshToken } = req.body;
const index = refreshTokens.indexOf(refreshToken);
if (index > -1) refreshTokens.splice(index, 1);
res.json({ message: 'Logged out' });
});
JWT vs Sessions
| Feature | JWT | Sessions |
|---|---|---|
| State | Stateless (no server storage) | Stateful (server stores data) |
| Scalability | Easy - no shared session store | Needs Redis/DB shared across instances |
| Payload | Visible in token (don’t put secrets) | Hidden on server |
| Revocation | Hard - can’t invalidate until expiry | Easy - delete from session store |
| Size | Larger (sent with every request) | Small (just session ID cookie) |
| Best for | APIs, mobile apps, microservices | Server-rendered web apps |
Storing Tokens on the Client
// ✅ Option 1: localStorage (simple but XSS-vulnerable)
localStorage.setItem('token', token);
const token = localStorage.getItem('token');
// ✅ Option 2: httpOnly cookie (XSS-safe but CSRF-vulnerable)
res.cookie('token', token, {
httpOnly: true,
secure: true,
sameSite: 'strict',
maxAge: 24 * 60 * 60 * 1000
});
// ✅ Option 3: Memory (most secure, but lost on refresh)
// Store in a closure variable
Token Blacklisting (for immediate revocation)
const blacklistedTokens = new Set();
function blacklistToken(token) {
const decoded = jwt.decode(token);
blacklistedTokens.add(token);
// Auto-remove after expiry
setTimeout(() => blacklistedTokens.delete(token),
(decoded.exp * 1000) - Date.now());
}
function authenticate(req, res, next) {
const token = req.headers['authorization']?.slice(7);
if (blacklistedTokens.has(token)) {
return res.status(401).json({ error: 'Token revoked' });
}
try {
req.user = jwt.verify(token, process.env.JWT_SECRET);
next();
} catch (err) {
res.status(403).json({ error: 'Invalid token' });
}
}
Key Takeaways
- JWT is stateless - no server-side storage needed
- Use
jwt.sign(payload, secret, { expiresIn })to create tokens - Use
authenticatemiddleware to verify tokens on protected routes - Use
authorizemiddleware for role-based access control - Store only non-sensitive data in the payload (userId, role)
- Use short-lived access tokens (15min) + long-lived refresh tokens (7d)
- Hash passwords with
bcryptbefore storing - Pass tokens via
Authorization: Bearer <token>header - JWT is best for APIs and mobile apps; sessions are better for server-rendered apps