JWT Implementation
JSON Web Tokens enable stateless authentication - the server doesn't need to store session data. Today you learn to generate, verify, and use JWTs to protect your API routes.
Understanding JWT
A JWT is a compact, URL-safe token containing claims (data) that can be verified and trusted because it is digitally signed.
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
|_______________________________|______________________________________________________________________|__________________________________|
HEADER PAYLOAD SIGNATURE
JWT Structure
- Header - Algorithm (HS256) and token type (JWT)
- Payload - Claims (user data, expiration, issued at)
- Signature - Verifies the token hasn't been tampered with
JWT payload is only Base64 encoded, NOT encrypted. Anyone can decode and read it. Never put sensitive data like passwords in the payload.
npm install jsonwebtoken
Generating Tokens
const jwt = require('jsonwebtoken');
// Secret key - in production, use environment variable
// Must be long, random, and kept secure
const JWT_SECRET = 'your-super-secret-key-min-32-chars';
// Generate a token
function generateToken(user) {
// Payload - data to encode in the token
const payload = {
id: user._id, // User's database ID
email: user.email, // User's email
role: user.role // User's role (optional)
};
// Sign the token with secret and options
const token = jwt.sign(payload, JWT_SECRET, {
expiresIn: '1h' // Token expires in 1 hour
});
return token;
}
// Verify a token
function verifyToken(token) {
try {
// Returns the decoded payload if valid
const decoded = jwt.verify(token, JWT_SECRET);
return decoded;
} catch (err) {
// Token is invalid or expired
return null;
}
}
Authentication Middleware
const jwt = require('jsonwebtoken');
const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key';
// Middleware to protect routes
function authenticate(req, res, next) {
// Get token from Authorization header
// Format: "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
const authHeader = req.headers.authorization;
// Check if header exists and has correct format
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({
error: 'Access denied. No token provided.'
});
}
// Extract token (remove "Bearer " prefix)
const token = authHeader.split(' ')[1];
try {
// Verify token and decode payload
const decoded = jwt.verify(token, JWT_SECRET);
// Attach user data to request object
// Now available in route handlers as req.user
req.user = decoded;
// Continue to the next middleware/route handler
next();
} catch (err) {
// Handle different JWT errors
if (err.name === 'TokenExpiredError') {
return res.status(401).json({ error: 'Token expired' });
}
return res.status(401).json({ error: 'Invalid token' });
}
}
// Role-based access control middleware
function authorize(...allowedRoles) {
return (req, res, next) => {
// Check if user's role is in the allowed list
if (!allowedRoles.includes(req.user.role)) {
return res.status(403).json({
error: 'Access denied. Insufficient permissions.'
});
}
next();
};
}
module.exports = { authenticate, authorize };
Complete JWT Authentication System
const express = require('express');
const mongoose = require('mongoose');
const bcrypt = require('bcrypt');
const jwt = require('jsonwebtoken');
const app = express();
app.use(express.json());
// Configuration - use environment variables in production
const JWT_SECRET = 'your-super-secret-key-change-in-production';
const JWT_EXPIRES_IN = '1h';
// Connect to MongoDB
mongoose.connect('mongodb://localhost:27017/jwtapp');
// User Schema
const userSchema = new mongoose.Schema({
email: { type: String, required: true, unique: true, lowercase: true },
password: { type: String, required: true, select: false },
name: { type: String, required: true },
role: { type: String, enum: ['user', 'admin'], default: 'user' }
});
// Hash password before saving
userSchema.pre('save', async function(next) {
if (!this.isModified('password')) return next();
this.password = await bcrypt.hash(this.password, 10);
next();
});
// Password comparison method
userSchema.methods.comparePassword = function(candidatePassword) {
return bcrypt.compare(candidatePassword, this.password);
};
const User = mongoose.model('User', userSchema);
// Generate JWT token
function generateToken(user) {
return jwt.sign(
{ id: user._id, email: user.email, role: user.role },
JWT_SECRET,
{ expiresIn: JWT_EXPIRES_IN }
);
}
// 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 {
req.user = jwt.verify(token, JWT_SECRET);
next();
} catch (err) {
res.status(401).json({ error: 'Invalid token' });
}
}
// Admin-only middleware
function adminOnly(req, res, next) {
if (req.user.role !== 'admin') {
return res.status(403).json({ error: 'Admin access required' });
}
next();
}
// ROUTES
// Register new user
app.post('/api/auth/register', async (req, res) => {
try {
const { email, password, name } = req.body;
const exists = await User.findOne({ email });
if (exists) {
return res.status(400).json({ error: 'Email already registered' });
}
const user = await User.create({ email, password, name });
const token = generateToken(user);
res.status(201).json({ token, user: { id: user._id, email: user.email, name: user.name } });
} catch (err) {
res.status(400).json({ error: err.message });
}
});
// Login user
app.post('/api/auth/login', async (req, res) => {
try {
const { email, password } = req.body;
const user = await User.findOne({ email }).select('+password');
if (!user || !(await user.comparePassword(password))) {
return res.status(401).json({ error: 'Invalid credentials' });
}
const token = generateToken(user);
res.json({ token, user: { id: user._id, email: user.email, name: user.name } });
} catch (err) {
res.status(500).json({ error: 'Login failed' });
}
});
// Protected route - get current user profile
app.get('/api/profile', authenticate, async (req, res) => {
const user = await User.findById(req.user.id);
res.json({ user: { id: user._id, email: user.email, name: user.name, role: user.role } });
});
// Admin-only route - get all users
app.get('/api/admin/users', authenticate, adminOnly, async (req, res) => {
const users = await User.find().select('-__v');
res.json({ users });
});
app.listen(3000, () => console.log('Server running on port 3000'));
Refresh Token Pattern
Access tokens should be short-lived for security. Refresh tokens allow getting new access tokens without re-login:
const ACCESS_TOKEN_EXPIRES = '15m'; // Short-lived access token
const REFRESH_TOKEN_EXPIRES = '7d'; // Long-lived refresh token
const REFRESH_SECRET = 'different-secret-for-refresh';
// Generate both tokens
function generateTokens(user) {
// Access token - short expiry, used for API requests
const accessToken = jwt.sign(
{ id: user._id, email: user.email, role: user.role },
JWT_SECRET,
{ expiresIn: ACCESS_TOKEN_EXPIRES }
);
// Refresh token - long expiry, only used to get new access tokens
const refreshToken = jwt.sign(
{ id: user._id },
REFRESH_SECRET,
{ expiresIn: REFRESH_TOKEN_EXPIRES }
);
return { accessToken, refreshToken };
}
// Login endpoint - returns both tokens
app.post('/api/auth/login', async (req, res) => {
// ... validation logic ...
const { accessToken, refreshToken } = generateTokens(user);
// Set refresh token as HTTP-only cookie (more secure)
res.cookie('refreshToken', refreshToken, {
httpOnly: true, // Not accessible via JavaScript
secure: true, // Only sent over HTTPS
sameSite: 'strict', // CSRF protection
maxAge: 7 * 24 * 60 * 60 * 1000 // 7 days in ms
});
res.json({ accessToken, user: { id: user._id, email: user.email } });
});
// Refresh endpoint - get new access token
app.post('/api/auth/refresh', (req, res) => {
// Get refresh token from cookie
const refreshToken = req.cookies.refreshToken;
if (!refreshToken) {
return res.status(401).json({ error: 'Refresh token required' });
}
try {
// Verify refresh token
const decoded = jwt.verify(refreshToken, REFRESH_SECRET);
// Generate new access token
const accessToken = jwt.sign(
{ id: decoded.id },
JWT_SECRET,
{ expiresIn: ACCESS_TOKEN_EXPIRES }
);
res.json({ accessToken });
} catch (err) {
res.status(401).json({ error: 'Invalid refresh token' });
}
});
// Logout - clear refresh token
app.post('/api/auth/logout', (req, res) => {
res.clearCookie('refreshToken');
res.json({ message: 'Logged out successfully' });
});
Video Resources
Knowledge Check
1. What is the purpose of the JWT signature?
2. Where should access tokens be sent in API requests?
3. Why should access tokens have short expiration times?
Congratulations on completing Days 6-10!
You've learned MongoDB, Mongoose, data modeling, authentication with bcrypt, and JWT implementation. You now have the skills to build secure, database-backed APIs. Continue to Days 11-15 to learn error handling, file uploads, testing, and deployment!