Express.js Middleware Patterns: Authentication and Authorization
A practical guide to Express.js authentication and authorization middleware covering JWT validation, role-based access control, API key authentication, and middleware composition.
Express.js Middleware Patterns: Authentication and Authorization
Authentication determines who a user is. Authorization determines what that user can do. In Express.js, both are implemented as middleware — functions that run before your route handlers, checking credentials and permissions before the request reaches your application logic.
Well-designed auth middleware is reusable, composable, and testable. You write it once and attach it to any route that needs protection. This guide covers practical patterns for building authentication and authorization middleware in Express.js applications.
Prerequisites
- Node.js installed (v16+)
- An Express.js application
- Basic understanding of HTTP headers and JSON Web Tokens
Middleware Fundamentals
Every Express middleware function receives three arguments: the request, the response, and a next function that passes control to the next middleware.
function myMiddleware(req, res, next) {
// Do something with the request
console.log(req.method, req.path);
// Pass control to the next middleware
next();
}
// Apply to all routes
app.use(myMiddleware);
// Apply to specific routes
app.get("/api/protected", myMiddleware, function(req, res) {
res.json({ message: "You passed the middleware" });
});
Auth middleware either calls next() to allow the request through or sends an error response to block it.
JWT Authentication Middleware
JSON Web Tokens are the most common authentication mechanism for APIs. The client sends a token in the Authorization header, and the middleware validates it.
Token Generation
// auth/token.js
var jwt = require("jsonwebtoken");
var SECRET = process.env.JWT_SECRET;
var EXPIRES_IN = "24h";
function generateToken(user) {
var payload = {
id: user.id,
email: user.email,
role: user.role
};
return jwt.sign(payload, SECRET, { expiresIn: EXPIRES_IN });
}
function generateRefreshToken(user) {
return jwt.sign(
{ id: user.id, type: "refresh" },
SECRET,
{ expiresIn: "7d" }
);
}
module.exports = {
generateToken: generateToken,
generateRefreshToken: generateRefreshToken
};
Authentication Middleware
// middleware/authenticate.js
var jwt = require("jsonwebtoken");
var SECRET = process.env.JWT_SECRET;
function authenticate(req, res, next) {
var header = req.headers.authorization;
if (!header) {
return res.status(401).json({ error: "No authorization header" });
}
var parts = header.split(" ");
if (parts.length !== 2 || parts[0] !== "Bearer") {
return res.status(401).json({ error: "Invalid authorization format. Use: Bearer <token>" });
}
var token = parts[1];
try {
var decoded = jwt.verify(token, SECRET);
req.user = decoded;
next();
} catch (err) {
if (err.name === "TokenExpiredError") {
return res.status(401).json({ error: "Token expired" });
}
if (err.name === "JsonWebTokenError") {
return res.status(401).json({ error: "Invalid token" });
}
return res.status(401).json({ error: "Authentication failed" });
}
}
module.exports = authenticate;
Using the Middleware
// routes/api.js
var express = require("express");
var router = express.Router();
var authenticate = require("../middleware/authenticate");
// Public routes — no authentication required
router.post("/login", function(req, res) {
// Validate credentials, generate token
});
router.post("/register", function(req, res) {
// Create user, generate token
});
// Protected routes — authentication required
router.get("/profile", authenticate, function(req, res) {
// req.user is available here
res.json({ user: req.user });
});
router.put("/profile", authenticate, function(req, res) {
// Update user profile
});
module.exports = router;
Protecting All Routes in a Group
// Apply authentication to all routes after this point
var publicRouter = express.Router();
var protectedRouter = express.Router();
publicRouter.post("/login", loginHandler);
publicRouter.post("/register", registerHandler);
protectedRouter.use(authenticate); // Everything below requires authentication
protectedRouter.get("/profile", profileHandler);
protectedRouter.get("/settings", settingsHandler);
protectedRouter.put("/profile", updateProfileHandler);
app.use("/api", publicRouter);
app.use("/api", protectedRouter);
Optional Authentication
Some routes work for both authenticated and anonymous users — showing different content based on login status:
// middleware/optionalAuth.js
var jwt = require("jsonwebtoken");
var SECRET = process.env.JWT_SECRET;
function optionalAuth(req, res, next) {
var header = req.headers.authorization;
if (!header) {
req.user = null;
return next();
}
var parts = header.split(" ");
if (parts.length !== 2 || parts[0] !== "Bearer") {
req.user = null;
return next();
}
try {
req.user = jwt.verify(parts[1], SECRET);
} catch (err) {
req.user = null;
}
next();
}
module.exports = optionalAuth;
var optionalAuth = require("../middleware/optionalAuth");
app.get("/api/articles", optionalAuth, function(req, res) {
if (req.user) {
// Show personalized content, bookmarks, etc.
} else {
// Show public content
}
});
Role-Based Authorization
Authentication confirms identity. Authorization checks permissions. After authenticate sets req.user, authorization middleware checks the user's role.
Simple Role Check
// middleware/authorize.js
function authorize() {
var allowedRoles = Array.prototype.slice.call(arguments);
return function(req, res, next) {
if (!req.user) {
return res.status(401).json({ error: "Authentication required" });
}
if (allowedRoles.length === 0) {
return next(); // No specific role required
}
if (allowedRoles.indexOf(req.user.role) === -1) {
return res.status(403).json({
error: "Forbidden. Required role: " + allowedRoles.join(" or ")
});
}
next();
};
}
module.exports = authorize;
var authenticate = require("../middleware/authenticate");
var authorize = require("../middleware/authorize");
// Any authenticated user
router.get("/profile", authenticate, function(req, res) { /* ... */ });
// Admin only
router.get("/admin/users", authenticate, authorize("admin"), function(req, res) { /* ... */ });
// Admin or editor
router.put("/articles/:id", authenticate, authorize("admin", "editor"), function(req, res) { /* ... */ });
// Admin, editor, or author
router.post("/articles", authenticate, authorize("admin", "editor", "author"), function(req, res) { /* ... */ });
Permission-Based Authorization
For finer-grained control, check specific permissions instead of roles:
// middleware/requirePermission.js
var ROLE_PERMISSIONS = {
admin: ["users:read", "users:write", "users:delete", "articles:read", "articles:write", "articles:delete", "settings:read", "settings:write"],
editor: ["articles:read", "articles:write", "articles:delete", "users:read"],
author: ["articles:read", "articles:write"],
viewer: ["articles:read"]
};
function requirePermission(permission) {
return function(req, res, next) {
if (!req.user) {
return res.status(401).json({ error: "Authentication required" });
}
var permissions = ROLE_PERMISSIONS[req.user.role] || [];
if (permissions.indexOf(permission) === -1) {
return res.status(403).json({
error: "Permission denied. Required: " + permission
});
}
next();
};
}
module.exports = requirePermission;
var requirePermission = require("../middleware/requirePermission");
router.get("/users", authenticate, requirePermission("users:read"), listUsers);
router.delete("/users/:id", authenticate, requirePermission("users:delete"), deleteUser);
router.put("/articles/:id", authenticate, requirePermission("articles:write"), updateArticle);
Resource-Level Authorization
Sometimes authorization depends on the specific resource — a user can edit their own articles but not others:
// middleware/authorizeResource.js
function authorizeResource(getResourceOwnerId) {
return function(req, res, next) {
if (!req.user) {
return res.status(401).json({ error: "Authentication required" });
}
// Admins bypass resource-level checks
if (req.user.role === "admin") {
return next();
}
getResourceOwnerId(req)
.then(function(ownerId) {
if (ownerId === null) {
return res.status(404).json({ error: "Resource not found" });
}
if (ownerId !== req.user.id) {
return res.status(403).json({ error: "You do not own this resource" });
}
next();
})
.catch(function(err) {
next(err);
});
};
}
module.exports = authorizeResource;
var db = require("../db");
var authorizeResource = require("../middleware/authorizeResource");
function getArticleOwner(req) {
return db.query("SELECT author_id FROM articles WHERE id = $1", [req.params.id])
.then(function(result) {
if (result.rows.length === 0) return null;
return result.rows[0].author_id;
});
}
router.put("/articles/:id",
authenticate,
authorizeResource(getArticleOwner),
updateArticle
);
router.delete("/articles/:id",
authenticate,
authorizeResource(getArticleOwner),
deleteArticle
);
API Key Authentication
For server-to-server communication, API keys are simpler than JWT:
// middleware/apiKeyAuth.js
var db = require("../db");
function apiKeyAuth(req, res, next) {
var apiKey = req.headers["x-api-key"] || req.query.api_key;
if (!apiKey) {
return res.status(401).json({ error: "API key required" });
}
db.query("SELECT * FROM api_keys WHERE key = $1 AND active = true", [apiKey])
.then(function(result) {
if (result.rows.length === 0) {
return res.status(401).json({ error: "Invalid API key" });
}
var keyRecord = result.rows[0];
// Update last used timestamp
db.query("UPDATE api_keys SET last_used_at = NOW() WHERE id = $1", [keyRecord.id]);
req.apiClient = {
id: keyRecord.client_id,
name: keyRecord.client_name,
permissions: keyRecord.permissions
};
next();
})
.catch(function(err) {
next(err);
});
}
module.exports = apiKeyAuth;
Supporting Multiple Auth Methods
// middleware/flexibleAuth.js
var jwt = require("jsonwebtoken");
var db = require("../db");
var SECRET = process.env.JWT_SECRET;
function flexibleAuth(req, res, next) {
// Try JWT first
var authHeader = req.headers.authorization;
if (authHeader && authHeader.startsWith("Bearer ")) {
try {
req.user = jwt.verify(authHeader.split(" ")[1], SECRET);
req.authMethod = "jwt";
return next();
} catch (err) {
return res.status(401).json({ error: "Invalid token" });
}
}
// Try API key
var apiKey = req.headers["x-api-key"];
if (apiKey) {
return db.query("SELECT * FROM api_keys WHERE key = $1 AND active = true", [apiKey])
.then(function(result) {
if (result.rows.length === 0) {
return res.status(401).json({ error: "Invalid API key" });
}
req.apiClient = result.rows[0];
req.authMethod = "api_key";
next();
})
.catch(next);
}
return res.status(401).json({ error: "Authentication required. Use Bearer token or X-API-Key header." });
}
module.exports = flexibleAuth;
Middleware Composition
Combining Multiple Middleware
Express runs middleware in order. Chain them for layered security:
// Authenticate, then authorize, then handle
router.delete("/admin/users/:id",
authenticate,
authorize("admin"),
function(req, res) {
// Only admins reach here
}
);
Creating Middleware Factories
// middleware/rateLimit.js
function rateLimit(options) {
var windowMs = options.windowMs || 60000;
var max = options.max || 100;
var requests = {};
// Clean up old entries every minute
setInterval(function() {
var now = Date.now();
Object.keys(requests).forEach(function(key) {
if (requests[key].resetTime < now) {
delete requests[key];
}
});
}, 60000);
return function(req, res, next) {
var key = req.ip;
var now = Date.now();
if (!requests[key] || requests[key].resetTime < now) {
requests[key] = { count: 0, resetTime: now + windowMs };
}
requests[key].count++;
if (requests[key].count > max) {
return res.status(429).json({
error: "Too many requests",
retryAfter: Math.ceil((requests[key].resetTime - now) / 1000)
});
}
next();
};
}
module.exports = rateLimit;
var rateLimit = require("../middleware/rateLimit");
// Strict rate limit on login attempts
router.post("/login",
rateLimit({ windowMs: 15 * 60 * 1000, max: 5 }),
loginHandler
);
// Moderate rate limit on API calls
router.use("/api",
authenticate,
rateLimit({ windowMs: 60 * 1000, max: 100 })
);
Composable Middleware Helper
// middleware/compose.js
function compose() {
var middlewares = Array.prototype.slice.call(arguments);
return function(req, res, next) {
var index = 0;
function run() {
if (index >= middlewares.length) {
return next();
}
var middleware = middlewares[index++];
middleware(req, res, function(err) {
if (err) return next(err);
run();
});
}
run();
};
}
module.exports = compose;
var compose = require("../middleware/compose");
// Create reusable middleware chains
var adminOnly = compose(authenticate, authorize("admin"));
var editorOrAdmin = compose(authenticate, authorize("admin", "editor"));
var rateLimitedAuth = compose(
rateLimit({ windowMs: 60000, max: 100 }),
authenticate
);
router.get("/admin/dashboard", adminOnly, dashboardHandler);
router.put("/articles/:id", editorOrAdmin, updateArticleHandler);
router.get("/api/data", rateLimitedAuth, dataHandler);
Login and Token Refresh
Login Route
// routes/auth.js
var bcrypt = require("bcrypt");
var db = require("../db");
var token = require("../auth/token");
router.post("/login", function(req, res) {
var email = req.body.email;
var password = req.body.password;
if (!email || !password) {
return res.status(400).json({ error: "Email and password required" });
}
db.query("SELECT * FROM users WHERE email = $1", [email])
.then(function(result) {
if (result.rows.length === 0) {
return res.status(401).json({ error: "Invalid credentials" });
}
var user = result.rows[0];
return bcrypt.compare(password, user.password_hash)
.then(function(match) {
if (!match) {
return res.status(401).json({ error: "Invalid credentials" });
}
var accessToken = token.generateToken(user);
var refreshToken = token.generateRefreshToken(user);
res.json({
accessToken: accessToken,
refreshToken: refreshToken,
user: { id: user.id, email: user.email, role: user.role }
});
});
})
.catch(function(err) {
console.error("Login error:", err);
res.status(500).json({ error: "Login failed" });
});
});
Token Refresh Route
router.post("/refresh", function(req, res) {
var refreshToken = req.body.refreshToken;
if (!refreshToken) {
return res.status(400).json({ error: "Refresh token required" });
}
try {
var decoded = jwt.verify(refreshToken, SECRET);
if (decoded.type !== "refresh") {
return res.status(401).json({ error: "Invalid refresh token" });
}
db.query("SELECT * FROM users WHERE id = $1", [decoded.id])
.then(function(result) {
if (result.rows.length === 0) {
return res.status(401).json({ error: "User not found" });
}
var user = result.rows[0];
var newAccessToken = token.generateToken(user);
res.json({ accessToken: newAccessToken });
});
} catch (err) {
return res.status(401).json({ error: "Invalid or expired refresh token" });
}
});
Common Issues and Troubleshooting
Token validation works locally but fails in production
Different JWT_SECRET values between environments:
Fix: Ensure the same secret is set in production environment variables. Use a strong, random secret (at least 256 bits). Never commit secrets to version control.
CORS errors when sending Authorization header
The browser's preflight OPTIONS request is blocked:
Fix: Configure CORS to allow the Authorization header. The CORS middleware must run before the authentication middleware so OPTIONS requests pass through.
"jwt malformed" errors
The token string is corrupted or not a valid JWT:
Fix: Check that the client sends the full token without truncation. Verify the token format — it should be three base64-encoded segments separated by dots. Check for extra whitespace.
Authorization passes for deleted users
The JWT contains a snapshot of user data from when the token was issued:
Fix: For sensitive operations, verify the user still exists in the database. Set shorter token expiration times. Implement a token blacklist for immediate revocation.
Best Practices
- Separate authentication from authorization. Authentication middleware validates the token and sets
req.user. Authorization middleware checksreq.user.roleor permissions. They are independent and reusable. - Use middleware factories for configurable behavior. Functions that return middleware allow you to parameterize behavior:
authorize("admin"),rateLimit({ max: 5 }). - Return consistent error responses. Always return JSON with an
errorfield. Use 401 for missing or invalid authentication, 403 for insufficient permissions. - Keep tokens short-lived. Access tokens should expire in hours, not days. Use refresh tokens for longer sessions.
- Never store passwords in plain text. Use bcrypt with a cost factor of 10-12 for password hashing.
- Rate limit authentication endpoints. Login and registration endpoints are targets for brute force attacks. Limit attempts per IP.
- Log authentication events. Log successful logins, failed attempts, and permission denials for security monitoring.
- Use HTTPS in production. Tokens sent over HTTP can be intercepted. Enforce HTTPS for all authenticated endpoints.