Introduction to JWT Authentication
JSON Web Tokens (JWT) have become the industry standard for token-based authentication in modern web applications and APIs. In this lecture, we'll take a deep dive into implementing JWT authentication in real-world applications.
JWTs offer a stateless authentication mechanism where the token itself contains all the necessary information about the authenticated user. Think of a JWT like a digital passport - it contains your identity details, is issued by a trusted authority, has security features to prevent forgery, and can be presented at multiple checkpoints without needing to verify your original credentials each time.
Anatomy of a JWT
Before implementing JWT authentication, let's understand the structure of a JSON Web Token.
A JWT consists of three parts, separated by dots:
header.payload.signature
Header
The header typically consists of two parts: the type of token (JWT) and the signing algorithm being used, such as HMAC SHA256 or RSA.
{
"alg": "HS256",
"typ": "JWT"
}
Payload
The payload contains the claims. Claims are statements about an entity (typically, the user) and additional data. There are three types of claims: registered, public, and private claims.
{
"sub": "1234567890", // Subject (user ID)
"name": "John Doe", // Custom claim
"admin": true, // Custom claim
"iat": 1516239022, // Issued At (timestamp)
"exp": 1516242622 // Expiration Time (timestamp)
}
Signature
The signature is created by taking the encoded header, the encoded payload, a secret, and the algorithm specified in the header, and signing them.
HMACSHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
secret
)
This signature ensures that the message hasn't been altered along the way. If someone tries to modify the payload, the signature would no longer match, and the token would be invalidated.
Visualizing a Real JWT
Here's an example of a real JWT:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMiwiZXhwIjoxNTE2MjQyNjIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
Each part can be decoded from base64Url:
- Header (red):
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 - Payload (purple):
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMiwiZXhwIjoxNTE2MjQyNjIyfQ - Signature (blue):
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
Important: Since JWTs are base64Url encoded but not encrypted, anyone can decode them to read the information inside. Never store sensitive information like passwords or credit card numbers in a JWT payload!
Setting Up JWT Authentication in Node.js
Let's implement JWT authentication in a Node.js application using Express.
Installing Required Packages
npm install express jsonwebtoken bcrypt mongoose dotenv cors
Project Structure
jwt-auth-demo/
├── .env # Environment variables
├── package.json # Project dependencies
├── server.js # Main Express server
├── config/
│ └── db.js # Database configuration
├── controllers/
│ └── authController.js # Authentication logic
├── middleware/
│ └── authMiddleware.js # JWT verification middleware
├── models/
│ └── User.js # User model
└── routes/
├── authRoutes.js # Authentication routes
└── userRoutes.js # Protected user routes
Environment Variables
Create a .env file to store your sensitive configurations:
MONGO_URI=mongodb://localhost:27017/jwt-auth-demo
JWT_SECRET=your_jwt_secret_key
JWT_EXPIRE=1h
PORT=3000
Database Configuration
Let's create a MongoDB connection using Mongoose:
// config/db.js
const mongoose = require('mongoose');
const connectDB = async () => {
try {
const conn = await mongoose.connect(process.env.MONGO_URI, {
useNewUrlParser: true,
useUnifiedTopology: true
});
console.log(`MongoDB Connected: ${conn.connection.host}`);
} catch (error) {
console.error(`Error: ${error.message}`);
process.exit(1);
}
};
module.exports = connectDB;
User Model
Create a User model with password hashing:
// models/User.js
const mongoose = require('mongoose');
const bcrypt = require('bcrypt');
const UserSchema = new mongoose.Schema({
username: {
type: String,
required: [true, 'Please provide a username'],
unique: true,
trim: true,
maxlength: [50, 'Username cannot be more than 50 characters']
},
email: {
type: String,
required: [true, 'Please provide an email'],
unique: true,
match: [
/^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,3})+$/,
'Please provide a valid email'
]
},
password: {
type: String,
required: [true, 'Please provide a password'],
minlength: [6, 'Password must be at least 6 characters'],
select: false // Don't return password by default
},
role: {
type: String,
enum: ['user', 'admin'],
default: 'user'
},
createdAt: {
type: Date,
default: Date.now
}
});
// Hash password before saving
UserSchema.pre('save', async function(next) {
// Only hash the password if it's modified
if (!this.isModified('password')) {
next();
}
const salt = await bcrypt.genSalt(10);
this.password = await bcrypt.hash(this.password, salt);
next();
});
// Method to check if password matches
UserSchema.methods.matchPassword = async function(enteredPassword) {
return await bcrypt.compare(enteredPassword, this.password);
};
// Method to get JWT token for this user
UserSchema.methods.getSignedJwtToken = function() {
return jwt.sign(
{ id: this._id, role: this.role },
process.env.JWT_SECRET,
{ expiresIn: process.env.JWT_EXPIRE }
);
};
module.exports = mongoose.model('User', UserSchema);
Authentication Controller
Create the authentication logic:
// controllers/authController.js
const jwt = require('jsonwebtoken');
const User = require('../models/User');
// Register a new user
exports.register = async (req, res) => {
try {
const { username, email, password } = req.body;
// Create user
const user = await User.create({
username,
email,
password
});
// Generate JWT
sendTokenResponse(user, 201, res);
} catch (error) {
res.status(400).json({
success: false,
error: error.message
});
}
};
// Login user
exports.login = async (req, res) => {
try {
const { email, password } = req.body;
// Validate email & password
if (!email || !password) {
return res.status(400).json({
success: false,
error: 'Please provide an email and password'
});
}
// Check for user
const user = await User.findOne({ email }).select('+password');
if (!user) {
return res.status(401).json({
success: false,
error: 'Invalid credentials'
});
}
// Check if password matches
const isMatch = await user.matchPassword(password);
if (!isMatch) {
return res.status(401).json({
success: false,
error: 'Invalid credentials'
});
}
// Generate JWT
sendTokenResponse(user, 200, res);
} catch (error) {
res.status(500).json({
success: false,
error: error.message
});
}
};
// Get current logged in user
exports.getMe = async (req, res) => {
try {
const user = await User.findById(req.user.id);
res.status(200).json({
success: true,
data: user
});
} catch (error) {
res.status(500).json({
success: false,
error: error.message
});
}
};
// Log user out / clear cookie
exports.logout = async (req, res) => {
res.status(200).json({
success: true,
data: {}
});
};
// Helper function to get token from model, create cookie and send response
const sendTokenResponse = (user, statusCode, res) => {
// Create token
const token = user.getSignedJwtToken();
res.status(statusCode).json({
success: true,
token
});
};
Authentication Middleware
Create middleware to protect routes:
// middleware/authMiddleware.js
const jwt = require('jsonwebtoken');
const User = require('../models/User');
exports.protect = async (req, res, next) => {
let token;
// Get token from Authorization header
if (
req.headers.authorization &&
req.headers.authorization.startsWith('Bearer')
) {
// Set token from Bearer token
token = req.headers.authorization.split(' ')[1];
}
// Check if token exists
if (!token) {
return res.status(401).json({
success: false,
error: 'Not authorized to access this route'
});
}
try {
// Verify token
const decoded = jwt.verify(token, process.env.JWT_SECRET);
// Add user from payload
req.user = await User.findById(decoded.id);
next();
} catch (error) {
return res.status(401).json({
success: false,
error: 'Not authorized to access this route'
});
}
};
// Grant access to specific roles
exports.authorize = (...roles) => {
return (req, res, next) => {
if (!roles.includes(req.user.role)) {
return res.status(403).json({
success: false,
error: `Role ${req.user.role} is not authorized to access this route`
});
}
next();
};
};
Authentication Routes
Create routes for authentication:
// routes/authRoutes.js
const express = require('express');
const { register, login, getMe, logout } = require('../controllers/authController');
const { protect } = require('../middleware/authMiddleware');
const router = express.Router();
router.post('/register', register);
router.post('/login', login);
router.get('/me', protect, getMe);
router.get('/logout', logout);
module.exports = router;
Protected Routes
Create some protected user routes:
// routes/userRoutes.js
const express = require('express');
const { protect, authorize } = require('../middleware/authMiddleware');
const router = express.Router();
router.get(
'/',
protect,
authorize('admin'),
(req, res) => {
res.status(200).json({
success: true,
message: 'You have access to all users'
});
}
);
router.get(
'/profile',
protect,
(req, res) => {
res.status(200).json({
success: true,
message: 'Your profile data',
user: req.user
});
}
);
module.exports = router;
Main Server File
Finally, create the main Express server file:
// server.js
const express = require('express');
const dotenv = require('dotenv');
const cors = require('cors');
const connectDB = require('./config/db');
// Load env vars
dotenv.config();
// Connect to database
connectDB();
// Route files
const authRoutes = require('./routes/authRoutes');
const userRoutes = require('./routes/userRoutes');
const app = express();
// Body parser
app.use(express.json());
// Enable CORS
app.use(cors());
// Mount routers
app.use('/api/auth', authRoutes);
app.use('/api/users', userRoutes);
// Basic route
app.get('/', (req, res) => {
res.send('API is running...');
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
// Handle unhandled promise rejections
process.on('unhandledRejection', (err, promise) => {
console.log(`Error: ${err.message}`);
// Close server & exit process
server.close(() => process.exit(1));
});
Running the Application
npm start
Now you have a complete JWT authentication system with user registration, login, and protected routes!
Advanced JWT Implementation Techniques
Let's explore some advanced techniques for using JWT in production applications.
Refresh Token Implementation
As discussed in our previous lecture, it's a good practice to use short-lived access tokens with a refresh token mechanism.
// Add to User model
UserSchema.methods.getRefreshToken = function() {
return jwt.sign(
{ id: this._id },
process.env.REFRESH_TOKEN_SECRET,
{ expiresIn: process.env.REFRESH_TOKEN_EXPIRE }
);
};
// models/Token.js - For storing refresh tokens
const mongoose = require('mongoose');
const TokenSchema = new mongoose.Schema({
token: {
type: String,
required: true
},
userId: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
required: true
},
type: {
type: String,
enum: ['refresh'],
required: true
},
expiresAt: {
type: Date,
required: true
},
createdAt: {
type: Date,
default: Date.now
}
});
module.exports = mongoose.model('Token', TokenSchema);
Modify the auth controller to include refresh tokens:
// Add to authController.js
exports.refreshToken = async (req, res) => {
try {
const { refreshToken } = req.body;
if (!refreshToken) {
return res.status(400).json({
success: false,
error: 'No refresh token provided'
});
}
// Verify refresh token
const decoded = jwt.verify(refreshToken, process.env.REFRESH_TOKEN_SECRET);
// Check if token exists in database
const storedToken = await Token.findOne({
token: refreshToken,
userId: decoded.id,
type: 'refresh'
});
if (!storedToken) {
return res.status(401).json({
success: false,
error: 'Invalid refresh token'
});
}
if (new Date() > storedToken.expiresAt) {
await Token.findByIdAndDelete(storedToken._id);
return res.status(401).json({
success: false,
error: 'Refresh token expired'
});
}
// Get user
const user = await User.findById(decoded.id);
if (!user) {
return res.status(401).json({
success: false,
error: 'User not found'
});
}
// Generate new access token
const accessToken = user.getSignedJwtToken();
res.status(200).json({
success: true,
accessToken
});
} catch (error) {
return res.status(401).json({
success: false,
error: 'Invalid refresh token'
});
}
};
// Update sendTokenResponse function
const sendTokenResponse = async (user, statusCode, res) => {
// Create tokens
const accessToken = user.getSignedJwtToken();
const refreshToken = user.getRefreshToken();
// Save refresh token to database
await Token.create({
token: refreshToken,
userId: user._id,
type: 'refresh',
expiresAt: new Date(Date.now() + parseInt(process.env.REFRESH_TOKEN_EXPIRE_MS))
});
res.status(statusCode).json({
success: true,
accessToken,
refreshToken
});
};
// Add to logout function
exports.logout = async (req, res) => {
try {
const { refreshToken } = req.body;
if (refreshToken) {
// Delete refresh token from database
await Token.findOneAndDelete({ token: refreshToken });
}
res.status(200).json({
success: true,
data: {}
});
} catch (error) {
res.status(500).json({
success: false,
error: error.message
});
}
};
Add the refresh token route:
// Add to authRoutes.js
router.post('/refresh-token', refreshToken);
JWT Token Blacklisting
When access tokens have a longer lifespan, you might need to blacklist them in some cases (e.g., after logout).
// models/BlacklistedToken.js
const mongoose = require('mongoose');
const BlacklistedTokenSchema = new mongoose.Schema({
token: {
type: String,
required: true,
unique: true
},
expiresAt: {
type: Date,
required: true
},
createdAt: {
type: Date,
default: Date.now,
expires: 86400 // Auto-delete after 24 hours using TTL index
}
});
module.exports = mongoose.model('BlacklistedToken', BlacklistedTokenSchema);
// Update authMiddleware.js
exports.protect = async (req, res, next) => {
let token;
// ... token extraction code ...
try {
// Check if token is blacklisted
const blacklisted = await BlacklistedToken.findOne({ token });
if (blacklisted) {
return res.status(401).json({
success: false,
error: 'Token has been revoked'
});
}
// Verify token
const decoded = jwt.verify(token, process.env.JWT_SECRET);
// Add user from payload
req.user = await User.findById(decoded.id);
next();
} catch (error) {
// ... error handling ...
}
};
// Update logout function
exports.logout = async (req, res) => {
try {
const { refreshToken } = req.body;
// Get access token from header
const authHeader = req.headers.authorization;
const accessToken = authHeader && authHeader.split(' ')[1];
if (accessToken) {
// Get token expiration from JWT
const decoded = jwt.decode(accessToken);
const expiresAt = new Date(decoded.exp * 1000);
// Add to blacklist
await BlacklistedToken.create({
token: accessToken,
expiresAt
});
}
if (refreshToken) {
// Delete refresh token from database
await Token.findOneAndDelete({ token: refreshToken });
}
res.status(200).json({
success: true,
data: {}
});
} catch (error) {
res.status(500).json({
success: false,
error: error.message
});
}
};
Important: Token blacklisting defeats one of the main advantages of JWTs (statelessness) since you now need to check a database for every request. This is why refresh tokens are generally preferred - keep access tokens short-lived so blacklisting isn't needed.
Security Considerations for JWT
JWTs come with their own set of security considerations that you should address when implementing.
Signature Algorithm Selection
Choose the appropriate algorithm for your security needs:
- HMAC (HS256, HS384, HS512): Symmetric algorithm where a single secret key is used for both signing and verifying
- RSA (RS256, RS384, RS512): Asymmetric algorithm using a private/public key pair
- ECDSA (ES256, ES384, ES512): Asymmetric algorithm with smaller key sizes than RSA
// Using RS256 (asymmetric)
const fs = require('fs');
const jwt = require('jsonwebtoken');
// Read private key
const privateKey = fs.readFileSync('private.key');
// Sign token
const token = jwt.sign(
{ userId: user._id },
privateKey,
{
algorithm: 'RS256',
expiresIn: '1h'
}
);
// On the verification side
const publicKey = fs.readFileSync('public.key');
// Verify token
try {
const decoded = jwt.verify(token, publicKey, { algorithms: ['RS256'] });
// Token is valid
} catch (error) {
// Token is invalid
}
Asymmetric algorithms are particularly useful in microservice architectures where the service that verifies tokens is different from the service that issues them.
Token Storage on the Client
Where to store JWTs on the client side is a contentious topic with different security trade-offs:
| Storage Method | Pros | Cons |
|---|---|---|
| Local Storage / Session Storage |
|
|
| HTTP-only Cookies |
|
|
| JavaScript Memory (Variables) |
|
|
Recommended Approach
A common pattern is to store the access token in memory (or a JavaScript variable) and the refresh token in an HTTP-only cookie:
// Server-side - Setting HTTP-only cookie for refresh token
exports.login = async (req, res) => {
// ... authentication logic ...
// Create tokens
const accessToken = user.getSignedJwtToken();
const refreshToken = user.getRefreshToken();
// Save refresh token to cookie
res.cookie('refreshToken', refreshToken, {
httpOnly: true, // Prevents JavaScript access
secure: process.env.NODE_ENV === 'production', // HTTPS only in production
sameSite: 'strict', // CSRF protection
maxAge: 30 * 24 * 60 * 60 * 1000 // 30 days
});
// Send access token in response
res.status(200).json({
success: true,
accessToken
});
};
// Client-side - Storing access token in memory
let accessToken = null;
async function login(email, password) {
try {
const response = await fetch('/api/auth/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ email, password }),
credentials: 'include' // Necessary for cookies
});
const data = await response.json();
if (data.success) {
// Store access token in memory
accessToken = data.accessToken;
return true;
}
return false;
} catch (error) {
console.error('Login error:', error);
return false;
}
}
async function refreshAccessToken() {
try {
const response = await fetch('/api/auth/refresh-token', {
method: 'POST',
credentials: 'include' // Send cookies
});
const data = await response.json();
if (data.success) {
// Update access token in memory
accessToken = data.accessToken;
return true;
}
return false;
} catch (error) {
console.error('Token refresh error:', error);
return false;
}
}
Common JWT Security Pitfalls
- Missing Signature Verification: Never trust a JWT without verifying its signature
- Weak Secrets: Use strong, randomly generated secrets for HMAC algorithms
- Not Validating Claims: Always check the expiration time and issuer
- Using the 'none' Algorithm: Explicitly specify allowed algorithms when verifying
- Storing Sensitive Data: Never put sensitive information in the payload
- Excessive Token Lifespan: Keep access tokens short-lived
// Bad - No algorithm restriction
jwt.verify(token, secret);
// Good - Restrict allowed algorithms
jwt.verify(token, secret, { algorithms: ['HS256'] });
Implementing JWT in Different Frontend Frameworks
Let's look at how to implement JWT authentication in popular frontend frameworks.
React with Axios
// src/services/authService.js
import axios from 'axios';
const API_URL = 'http://localhost:3000/api/auth/';
// Create axios instance
const api = axios.create({
baseURL: 'http://localhost:3000/api',
headers: {
'Content-Type': 'application/json'
}
});
// Add a request interceptor to add the auth token
api.interceptors.request.use(
(config) => {
const token = getAccessToken();
if (token) {
config.headers['Authorization'] = `Bearer ${token}`;
}
return config;
},
(error) => {
return Promise.reject(error);
}
);
// Add a response interceptor to handle token refresh
api.interceptors.response.use(
(response) => response,
async (error) => {
const originalRequest = error.config;
// If error is not 401 or request has already been retried, reject
if (error.response.status !== 401 || originalRequest._retry) {
return Promise.reject(error);
}
originalRequest._retry = true;
try {
// Attempt to refresh the token
const { data } = await axios.post(API_URL + 'refresh-token', {}, {
withCredentials: true // Send cookies
});
// If token refresh is successful
if (data.accessToken) {
// Update the token in storage
setAccessToken(data.accessToken);
// Update the authorization header
originalRequest.headers['Authorization'] = `Bearer ${data.accessToken}`;
// Retry the original request
return api(originalRequest);
}
// If refresh fails, logout
logout();
return Promise.reject(error);
} catch (refreshError) {
// If refresh fails, logout
logout();
return Promise.reject(refreshError);
}
}
);
// Access token management
let inMemoryToken = null;
function getAccessToken() {
return inMemoryToken;
}
function setAccessToken(token) {
inMemoryToken = token;
}
// Auth service functions
export const register = async (username, email, password) => {
const response = await axios.post(API_URL + 'register', {
username,
email,
password
});
if (response.data.accessToken) {
setAccessToken(response.data.accessToken);
}
return response.data;
};
export const login = async (email, password) => {
const response = await axios.post(API_URL + 'login', {
email,
password
}, {
withCredentials: true // For refresh token cookie
});
if (response.data.accessToken) {
setAccessToken(response.data.accessToken);
}
return response.data;
};
export const logout = () => {
axios.post(API_URL + 'logout', {}, {
withCredentials: true
});
setAccessToken(null);
};
export const getCurrentUser = async () => {
return api.get('/auth/me');
};
export default api;
Vue with Vuex
// src/store/auth.module.js
import AuthService from '../services/auth.service';
const user = JSON.parse(localStorage.getItem('user'));
const initialState = user
? { status: { loggedIn: true }, user }
: { status: { loggedIn: false }, user: null };
export const auth = {
namespaced: true,
state: initialState,
actions: {
login({ commit }, { email, password }) {
return AuthService.login(email, password).then(
user => {
commit('loginSuccess', user);
return Promise.resolve(user);
},
error => {
commit('loginFailure');
return Promise.reject(error);
}
);
},
logout({ commit }) {
AuthService.logout();
commit('logout');
},
register({ commit }, { username, email, password }) {
return AuthService.register(username, email, password).then(
response => {
commit('registerSuccess');
return Promise.resolve(response.data);
},
error => {
commit('registerFailure');
return Promise.reject(error);
}
);
}
},
mutations: {
loginSuccess(state, user) {
state.status.loggedIn = true;
state.user = user;
},
loginFailure(state) {
state.status.loggedIn = false;
state.user = null;
},
logout(state) {
state.status.loggedIn = false;
state.user = null;
},
registerSuccess(state) {
state.status.loggedIn = false;
},
registerFailure(state) {
state.status.loggedIn = false;
}
}
};
Angular with HttpInterceptor
// src/app/helpers/auth.interceptor.ts
import { Injectable } from '@angular/core';
import {
HttpRequest,
HttpHandler,
HttpEvent,
HttpInterceptor,
HTTP_INTERCEPTORS,
HttpErrorResponse
} from '@angular/common/http';
import { Observable, throwError, BehaviorSubject } from 'rxjs';
import { catchError, filter, take, switchMap } from 'rxjs/operators';
import { TokenStorageService } from '../services/token-storage.service';
import { AuthService } from '../services/auth.service';
@Injectable()
export class AuthInterceptor implements HttpInterceptor {
private isRefreshing = false;
private refreshTokenSubject: BehaviorSubject = new BehaviorSubject(null);
constructor(
private tokenService: TokenStorageService,
private authService: AuthService
) {}
intercept(req: HttpRequest, next: HttpHandler): Observable> {
let authReq = req;
const token = this.tokenService.getToken();
if (token != null) {
authReq = this.addTokenHeader(req, token);
}
return next.handle(authReq).pipe(
catchError(error => {
if (error instanceof HttpErrorResponse && error.status === 401) {
return this.handle401Error(authReq, next);
}
return throwError(error);
})
);
}
private handle401Error(request: HttpRequest, next: HttpHandler) {
if (!this.isRefreshing) {
this.isRefreshing = true;
this.refreshTokenSubject.next(null);
return this.authService.refreshToken().pipe(
switchMap((token: any) => {
this.isRefreshing = false;
this.refreshTokenSubject.next(token.accessToken);
return next.handle(this.addTokenHeader(request, token.accessToken));
}),
catchError((err) => {
this.isRefreshing = false;
this.tokenService.signOut();
return throwError(err);
})
);
}
return this.refreshTokenSubject.pipe(
filter(token => token !== null),
take(1),
switchMap(token => next.handle(this.addTokenHeader(request, token)))
);
}
private addTokenHeader(request: HttpRequest, token: string) {
return request.clone({
headers: request.headers.set('Authorization', `Bearer ${token}`)
});
}
}
export const authInterceptorProviders = [
{ provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true }
];
Testing JWT Authentication
Proper testing is crucial for ensuring your JWT implementation is secure.
Unit Testing Authentication Controller
// tests/authController.test.js
const jwt = require('jsonwebtoken');
const mongoose = require('mongoose');
const request = require('supertest');
const app = require('../server');
const User = require('../models/User');
describe('Authentication Controller', () => {
let user;
beforeAll(async () => {
await mongoose.connect(process.env.MONGO_URI_TEST);
// Create a test user
user = await User.create({
username: 'testuser',
email: 'test@example.com',
password: 'password123'
});
});
afterAll(async () => {
await User.deleteMany({});
await mongoose.connection.close();
});
describe('POST /api/auth/register', () => {
it('should register a new user and return a token', async () => {
const res = await request(app)
.post('/api/auth/register')
.send({
username: 'newuser',
email: 'new@example.com',
password: 'password123'
});
expect(res.statusCode).toEqual(201);
expect(res.body).toHaveProperty('token');
expect(res.body).toHaveProperty('success', true);
// Verify token is valid
const decoded = jwt.verify(res.body.token, process.env.JWT_SECRET);
expect(decoded).toHaveProperty('id');
});
it('should not register a user with existing email', async () => {
const res = await request(app)
.post('/api/auth/register')
.send({
username: 'duplicate',
email: 'test@example.com', // Already exists
password: 'password123'
});
expect(res.statusCode).toEqual(400);
expect(res.body).toHaveProperty('success', false);
});
});
describe('POST /api/auth/login', () => {
it('should login user and return a token', async () => {
const res = await request(app)
.post('/api/auth/login')
.send({
email: 'test@example.com',
password: 'password123'
});
expect(res.statusCode).toEqual(200);
expect(res.body).toHaveProperty('token');
expect(res.body).toHaveProperty('success', true);
});
it('should not login with incorrect password', async () => {
const res = await request(app)
.post('/api/auth/login')
.send({
email: 'test@example.com',
password: 'wrongpassword'
});
expect(res.statusCode).toEqual(401);
expect(res.body).toHaveProperty('success', false);
});
});
describe('GET /api/auth/me', () => {
it('should get current user with auth token', async () => {
// Login to get token
const login = await request(app)
.post('/api/auth/login')
.send({
email: 'test@example.com',
password: 'password123'
});
const token = login.body.token;
// Get current user
const res = await request(app)
.get('/api/auth/me')
.set('Authorization', `Bearer ${token}`);
expect(res.statusCode).toEqual(200);
expect(res.body.data).toHaveProperty('email', 'test@example.com');
});
it('should not get user without auth token', async () => {
const res = await request(app)
.get('/api/auth/me');
expect(res.statusCode).toEqual(401);
});
});
});
Integration Testing Protected Routes
// tests/userRoutes.test.js
const jwt = require('jsonwebtoken');
const mongoose = require('mongoose');
const request = require('supertest');
const app = require('../server');
const User = require('../models/User');
describe('User Routes', () => {
let adminToken, userToken;
beforeAll(async () => {
await mongoose.connect(process.env.MONGO_URI_TEST);
// Create admin user
const admin = await User.create({
username: 'admin',
email: 'admin@example.com',
password: 'password123',
role: 'admin'
});
// Create normal user
const user = await User.create({
username: 'normaluser',
email: 'user@example.com',
password: 'password123',
role: 'user'
});
// Generate tokens
adminToken = jwt.sign(
{ id: admin._id, role: 'admin' },
process.env.JWT_SECRET,
{ expiresIn: '1h' }
);
userToken = jwt.sign(
{ id: user._id, role: 'user' },
process.env.JWT_SECRET,
{ expiresIn: '1h' }
);
});
afterAll(async () => {
await User.deleteMany({});
await mongoose.connection.close();
});
describe('GET /api/users', () => {
it('should allow admin to access all users', async () => {
const res = await request(app)
.get('/api/users')
.set('Authorization', `Bearer ${adminToken}`);
expect(res.statusCode).toEqual(200);
expect(res.body).toHaveProperty('success', true);
});
it('should not allow normal user to access all users', async () => {
const res = await request(app)
.get('/api/users')
.set('Authorization', `Bearer ${userToken}`);
expect(res.statusCode).toEqual(403);
});
});
describe('GET /api/users/profile', () => {
it('should allow authenticated user to access their profile', async () => {
const res = await request(app)
.get('/api/users/profile')
.set('Authorization', `Bearer ${userToken}`);
expect(res.statusCode).toEqual(200);
expect(res.body).toHaveProperty('success', true);
expect(res.body).toHaveProperty('user');
});
it('should not allow unauthenticated access', async () => {
const res = await request(app)
.get('/api/users/profile');
expect(res.statusCode).toEqual(401);
});
});
});
Performance Testing
For production applications, it's important to test the performance impact of your JWT implementation, especially if using blacklisting:
// Load testing with Artillery
// config.yml
config:
target: "http://localhost:3000"
phases:
- duration: 60
arrivalRate: 20
defaults:
headers:
Content-Type: "application/json"
scenarios:
- name: "Login and access protected route"
flow:
- post:
url: "/api/auth/login"
json:
email: "{{ $loopElement.email }}"
password: "{{ $loopElement.password }}"
capture:
- json: "$.token"
as: "authToken"
- get:
url: "/api/users/profile"
headers:
Authorization: "Bearer {{ authToken }}"
config:
payload:
- path: "users.csv"
fields:
- "email"
- "password"
skipHeader: true
Best Practices and Common Pitfalls
JWT Best Practices Summary
- Keep Tokens Small: Only include necessary data in the payload
- Use Short Expiration Times: Access tokens should typically expire in 15-60 minutes
- Implement Refresh Tokens: For better security and user experience
- Use HTTPS: Always transmit tokens over secure connections
- Consider Asymmetric Keys: Use RS256 instead of HS256 for larger systems
- Implement Proper Error Handling: Don't expose token validation details
- Validate All User Input: Prevent injection attacks
- Include Only Standard Claims: Stick to established JWT claims when possible
- Use a Strong Secret Key: At least 256 bits of entropy
- Monitor Token Usage: Implement logging for authentication events
Common Pitfalls to Avoid
- Storing Sensitive Data: The JWT payload is encoded, not encrypted
- Using Tokens for Sessions: JWTs aren't meant to replace traditional sessions completely
- Excessive Claims: Adding too much data increases token size and network overhead
- Missing Expiration: Always include an expiration time
- Using Weak Algorithms: Avoid using "none" or outdated algorithms
- Not Handling Refresh Token Rotation: Consider rotating refresh tokens for higher security
- Ignoring Token Revocation: Have a strategy for invalidating tokens when needed
Real-World Case Studies
Case Study 1: E-commerce Platform
An e-commerce platform with varying permission levels needs a robust authentication system:
Implementation Approach:
- JWT with RS256 algorithm (asymmetric keys)
- Access tokens with 15-minute expiration
- Refresh tokens with 7-day expiration, stored in HTTP-only cookies
- Role-based access control embedded in JWT claims
- Refresh token rotation on each use
- Centralized authentication service with API gateway integration
Case Study 2: Mobile Banking App
A mobile banking application with high-security requirements:
Implementation Approach:
- JWT with ES512 algorithm (modern elliptic curve)
- Very short-lived access tokens (5 minutes)
- Device-bound refresh tokens
- Step-up authentication for sensitive operations
- Mandatory MFA for all users
- Biometric authentication integration on mobile
- Secure storage using Keychain/Keystore on mobile
- Comprehensive audit logging
Practical Activities
Activity 1: Basic JWT Implementation
Build a simple Express API with JWT authentication:
- Create an Express server with MongoDB connection
- Implement user registration and login with JWT token generation
- Create a middleware to protect routes
- Add at least three protected endpoints
- Test the implementation using Postman or curl
Activity 2: Advanced JWT with Refresh Tokens
Extend your implementation with refresh tokens:
- Modify the login route to provide both access and refresh tokens
- Create a token refresh endpoint
- Implement server-side refresh token storage
- Add logout functionality to invalidate tokens
- Handle token expiration on the client side
Activity 3: Role-Based Authorization
Add role-based access control to your JWT system:
- Add role field to the user model
- Include role information in JWT payload
- Create middleware to check for specific roles
- Implement different access levels (public, user, admin)
- Test access control with different user roles
Activity 4: Security Testing
Conduct security tests on your JWT implementation:
- Test invalid tokens and signature tampering
- Test token expiration handling
- Try to access protected routes with incorrect permissions
- Implement rate limiting for login attempts
- Document security vulnerabilities and fixes
Additional Resources
Libraries and Tools
- jsonwebtoken - JWT implementation for Node.js
- JWT.io - JWT debugging tool
- hapi-auth-jwt2 - JWT authentication for Hapi.js
- jwt-auth - JWT authentication for Laravel
- PyJWT - JWT implementation for Python
Documentation and Specifications
- RFC 7519 - JSON Web Token
- RFC 7515 - JSON Web Signature
- RFC 7518 - JSON Web Algorithms
- Auth0 JWT Documentation