Authentication with JWT
Authentication with JWT
Authentication is one of the most critical features in any web application. Get it wrong and you expose user data. JWT (JSON Web Token) is the most widely used stateless authentication approach for REST APIs and single-page applications.
How JWT Authentication Works
1. User sends credentials (email + password) to server
2. Server verifies credentials against database
3. Server creates a signed JWT containing user data (ID, role, etc.)
4. Server returns JWT to client
5. Client stores JWT (localStorage or httpOnly cookie)
6. Client sends JWT in every subsequent request header
7. Server verifies JWT signature — no database lookup needed
8. If valid, server processes the request
The key insight: JWTs are self-contained. The server doesn't need to look up a session in a database. It just verifies the cryptographic signature.
JWT Structure
A JWT looks like: eyJhbG... (three Base64 parts separated by dots)
header.payload.signature
Header: {"alg": "HS256", "typ": "JWT"}
Payload: {"userId": "123", "role": "admin", "iat": 1716700800, "exp": 1716787200}
Signature: HMACSHA256(base64(header) + "." + base64(payload), secretKey)
The signature proves the token wasn't tampered with. The payload is readable by anyone — never put passwords or sensitive data in it.
Setting Up JWT in Node.js/Express
// Install: npm install express jsonwebtoken bcryptjs
const express = require('express');
const jwt = require('jsonwebtoken');
const bcrypt = require('bcryptjs');
const app = express();
app.use(express.json());
const JWT_SECRET = process.env.JWT_SECRET; // Strong random string, from env var
const JWT_EXPIRY = '24h';
// Mock user database
const users = [];
Registration Endpoint
app.post('/auth/register', async (req, res) => {
try {
const { email, password, name } = req.body;
// Validate input
if (!email || !password || !name) {
return res.status(400).json({ error: 'All fields required' });
}
if (password.length < 8) {
return res.status(400).json({ error: 'Password must be at least 8 characters' });
}
// Check if user exists
if (users.find(u => u.email === email)) {
return res.status(409).json({ error: 'Email already registered' });
}
// Hash password — NEVER store plain text
const passwordHash = await bcrypt.hash(password, 12);
// Store user
const user = { id: users.length + 1, email, name, passwordHash, role: 'user' };
users.push(user);
// Create token
const token = jwt.sign(
{ userId: user.id, email: user.email, role: user.role },
JWT_SECRET,
{ expiresIn: JWT_EXPIRY }
);
res.status(201).json({ token, user: { id: user.id, name, email } });
} catch (error) {
res.status(500).json({ error: 'Registration failed' });
}
});
Login Endpoint
app.post('/auth/login', async (req, res) => {
try {
const { email, password } = req.body;
// Find user
const user = users.find(u => u.email === email);
// Always use the same generic error — don't reveal which part failed
if (!user) {
return res.status(401).json({ error: 'Invalid credentials' });
}
// Verify password using constant-time comparison (bcrypt does this)
const isValid = await bcrypt.compare(password, user.passwordHash);
if (!isValid) {
return res.status(401).json({ error: 'Invalid credentials' });
}
// Create token
const token = jwt.sign(
{ userId: user.id, email: user.email, role: user.role },
JWT_SECRET,
{ expiresIn: JWT_EXPIRY }
);
res.json({ token, user: { id: user.id, name: user.name, email: user.email } });
} catch (error) {
res.status(500).json({ error: 'Login failed' });
}
});
Authentication Middleware
function authenticate(req, res, next) {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({ error: 'No token provided' });
}
const token = authHeader.split(' ')[1];
try {
const decoded = jwt.verify(token, JWT_SECRET);
req.user = decoded; // Attach user data to request
next();
} catch (error) {
if (error.name === 'TokenExpiredError') {
return res.status(401).json({ error: 'Token expired' });
}
return res.status(401).json({ error: 'Invalid token' });
}
}
// Role-based authorization middleware
function authorize(...roles) {
return (req, res, next) => {
if (!roles.includes(req.user.role)) {
return res.status(403).json({ error: 'Insufficient permissions' });
}
next();
};
}
Protected Routes
// Any logged-in user
app.get('/profile', authenticate, (req, res) => {
const user = users.find(u => u.id === req.user.userId);
res.json({ user: { id: user.id, name: user.name, email: user.email } });
});
// Admin only
app.get('/admin/users', authenticate, authorize('admin'), (req, res) => {
const allUsers = users.map(u => ({ id: u.id, name: u.name, email: u.email }));
res.json(allUsers);
});
// Multiple roles
app.put('/posts/:id', authenticate, authorize('admin', 'moderator'), (req, res) => {
// ...
});
Client-Side: Sending JWT with Requests
// Store token (httpOnly cookies are more secure, but localStorage works for demos)
localStorage.setItem('token', token);
// Attach to every request
const token = localStorage.getItem('token');
const response = await fetch('/api/profile', {
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
},
});
// Axios interceptor — attach token automatically
axios.interceptors.request.use(config => {
const token = localStorage.getItem('token');
if (token) config.headers.Authorization = `Bearer ${token}`;
return config;
});
Refresh Tokens
JWTs should be short-lived (15min–24h). Refresh tokens solve the "logged out too often" problem:
// Two-token strategy
// Access token: short-lived (15 min), sent with every request
// Refresh token: long-lived (30 days), stored in httpOnly cookie, used only to get new access tokens
app.post('/auth/refresh', (req, res) => {
const refreshToken = req.cookies.refreshToken;
if (!refreshToken) return res.status(401).json({ error: 'No refresh token' });
try {
const decoded = jwt.verify(refreshToken, REFRESH_SECRET);
const newAccessToken = jwt.sign(
{ userId: decoded.userId, email: decoded.email, role: decoded.role },
JWT_SECRET,
{ expiresIn: '15m' }
);
res.json({ accessToken: newAccessToken });
} catch {
res.status(401).json({ error: 'Invalid refresh token' });
}
});
Security Checklist
- Use
httpOnlycookies for tokens in production (prevents XSS) - Always use
httpsin production (prevents token interception) - Keep JWT secrets in environment variables, never in code
- Set reasonable expiry times (15min–24h for access tokens)
- Implement token refresh for better UX
- Never store passwords in plain text — always hash with bcrypt
- Use the same generic error message for "email not found" AND "wrong password"
- Set rate limiting on login endpoints to prevent brute-force attacks
Next lesson: File Uploads & Middleware — handling file uploads and building reusable middleware.
Get this course's notes on Telegram!
Free cheat sheets, summaries & practice exercises