Authentication Middleware for Express — Building Reusable Auth Components in Node.js
In this tutorial, you will learn about Authentication Middleware for Express. We cover key concepts, practical examples, and best practices to help you master this topic.
Express authentication middleware intercepts incoming requests before route handlers, validating credentials and attaching user context to the request object for downstream use.
What You'll Learn
Building Express middleware for JWT verification, API key validation, role-based access control, composing middleware chains, and handling authentication errors gracefully.
Why It Matters
Express middleware is the standard pattern for authentication in Node.js APIs. Centralizing auth logic in middleware prevents repetition, ensures consistency, and simplifies testing.
Real-World Use
Express.js powers APIs for companies like PayPal, Uber, and MySpace. Durga Antivirus Pro uses Express middleware for its dashboard API, combining JWT, API key, and session-based auth strategies.
Code Example: JWT Authentication Middleware
const jwt = require('jsonwebtoken');
const JWT_SECRET = process.env.JWT_SECRET || 'dev-secret';
function authenticateJWT(req, res, next) {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({
error: 'UNAUTHORIZED',
message: 'Missing or invalid Authorization header'
});
}
const token = authHeader.split(' ')[1];
try {
const decoded = jwt.verify(token, JWT_SECRET, {
algorithms: ['HS256'],
issuer: 'auth.durga-antivirus.com'
});
// Attach user to request
req.user = {
id: decoded.sub,
roles: decoded.roles || [],
scopes: (decoded.scope || '').split(' '),
sessionId: decoded.jti
};
next();
} catch (err) {
if (err.name === 'TokenExpiredError') {
return res.status(401).json({
error: 'TOKEN_EXPIRED',
message: 'Access token has expired'
});
}
return res.status(401).json({
error: 'INVALID_TOKEN',
message: 'Token validation failed'
});
}
}
Code Example: Role-Based Authorization Middleware
// Role-based access control middleware
function requireRole(...allowedRoles) {
return (req, res, next) => {
if (!req.user) {
return res.status(401).json({
error: 'UNAUTHORIZED',
message: 'Authentication required'
});
}
const userRoles = req.user.roles;
const hasRole = allowedRoles.some(role => userRoles.includes(role));
if (!hasRole) {
return res.status(403).json({
error: 'FORBIDDEN',
message: `Requires one of roles: ${allowedRoles.join(', ')}`,
userRoles: userRoles
});
}
next();
};
}
// Scope-based authorization middleware
function requireScope(requiredScope) {
return (req, res, next) => {
if (!req.user || !req.user.scopes) {
return res.status(401).json({ error: 'Authentication required' });
}
if (!req.user.scopes.includes(requiredScope)) {
return res.status(403).json({
error: 'INSUFFICIENT_SCOPE',
required: requiredScope,
granted: req.user.scopes
});
}
next();
};
}
// Route usage
router.get('/api/v1/threats',
authenticateJWT,
requireRole('analyst', 'admin'),
requireScope('threat:read'),
threatController.list
);
router.post('/api/v1/threats',
authenticateJWT,
requireRole('admin'),
requireScope('threat:write'),
threatController.create
);
Code Example: Composable Auth Middleware Chain
const compose = (...middlewares) => {
return (req, res, next) => {
let index = 0;
const execute = (err) => {
if (err) return next(err);
const middleware = middlewares[index++];
if (!middleware) return next();
try {
middleware(req, res, execute);
} catch (error) {
next(error);
}
};
execute();
};
};
// Auth strategy factory — supports multiple auth methods
function createAuthMiddleware(strategies = []) {
return async (req, res, next) => {
for (const strategy of strategies) {
const result = await strategy.authenticate(req);
if (result.authenticated) {
req.user = result.user;
req.authStrategy = strategy.name;
return next();
}
if (result.challenge) {
// Strategy can respond with WWW-Authenticate header
res.set('WWW-Authenticate', result.challenge);
}
}
return res.status(401).json({
error: 'UNAUTHORIZED',
message: 'No valid authentication provided'
});
};
}
// Define strategies
const jwtStrategy = {
name: 'jwt',
authenticate: async (req) => {
const header = req.headers.authorization;
if (!header || !header.startsWith('Bearer ')) {
return { authenticated: false };
}
try {
const decoded = jwt.verify(header.split(' ')[1], JWT_SECRET);
return { authenticated: true, user: { id: decoded.sub, roles: decoded.roles } };
} catch {
return { authenticated: false };
}
}
};
const apiKeyStrategy = {
name: 'api_key',
authenticate: async (req) => {
const key = req.headers['x-api-key'];
if (!key) return { authenticated: false };
const keyData = await validateApiKey(key);
if (keyData) {
return { authenticated: true, user: { id: keyData.service, roles: ['service'] } };
}
return { authenticated: false };
}
};
// Use composable auth
app.use('/api', createAuthMiddleware([jwtStrategy, apiKeyStrategy]));
Common Mistakes
1. Not Using try-catch with jwt.verify
jwt.verify throws on invalid tokens. Without try-catch, the error propagates to Express error handlers, potentially leaking stack traces.
2. Ignoring Token Algorithm
Always specify the allowed algorithms (algorithms: ['HS256']). Otherwise, an attacker can craft a token with algorithm 'none' and bypass verification.
3. Modifying req.body in Middleware
Middleware should attach to req.user, not modify req.body. Modifying the body can break downstream Parsing and validation.
4. Not Returning After res.status().json()
Without the return statement, Express continues to route handlers after sending an error response, causing multiple response attempts.
5. Applying Middleware Globally When Not Needed
Global middleware applies to every route. Use route-level middleware for auth to allow public endpoints (health, login) to bypass authentication.
Practice Questions
- How does Express middleware pass user data to route handlers?
- What is the difference between role-based and scope-based authorization?
- How does middleware composition work for multiple auth strategies?
- Why should jwt.verify specify allowed algorithms?
- What happens without the return statement after res.status().json()?
Answers:
- Middleware attaches data to the req object (req.user = decoded). Route handlers access this data via the same req parameter.
- Role-based checks the user's role (admin, analyst). Scope-based checks granular permissions (threat:read). Roles are broad categories; scopes are specific permissions.
- Each Strategy in the array attempts authentication. If one succeeds, the user is authenticated. If all fail, a 401 is returned. This allows multiple auth methods.
- If algorithms are not specified, an attacker can change the algorithm in the JWT header to 'none' and send a token with no signature, which jwt.verify accepts without verification.
- Express continues executing subsequent middleware and route handlers. The response may be attempted multiple times, causing "Cannot set headers after they are sent" errors.
Challenge: Build an Express auth middleware suite with JWT, API key, and Basic Auth strategies, composable chains, role and scope enforcement, and proper error responses.
FAQ
Mini Project
Build an Express API with composable auth middleware supporting JWT, API keys, and Basic Auth, with role and scope enforcement, proper error responses, and a test suite using supertest.
What's Next
Now learn about Authentication Middleware for FastAPI for building reusable auth components in Python APIs.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro