The protect middleware is the gateway to every authenticated route in your Express API. It runs before route handlers for protected endpoints, extracts the JWT from the Authorization header, verifies it, fetches the current user from MongoDB, and attaches them to req.user. The route handler then has access to the authenticated user without repeating any auth logic. In this lesson you will build the complete protect middleware, an authorize factory for role-based access control, and verify every scenario โ valid token, expired token, missing token, and insufficient permissions.
The protect Middleware โ Complete Implementation
// server/src/middleware/auth.js
const User = require('../models/User');
const AppError = require('../utils/AppError');
const { verifyAccessToken } = require('../utils/jwt');
const protect = async (req, res, next) => {
try {
// โโ 1. Extract token from Authorization header โโโโโโโโโโโโโโโโโโโโโโโโโ
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
throw new AppError('No token provided โ please log in', 401);
}
const token = authHeader.split(' ')[1]; // 'Bearer <token>' โ '<token>'
// โโ 2. Verify the token โ throws if invalid or expired โโโโโโโโโโโโโโโโโ
const decoded = verifyAccessToken(token); // throws TokenExpiredError or JsonWebTokenError
// โโ 3. Fetch the current user from MongoDB โโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// Always fetch from DB โ the token payload may be stale (role changed etc.)
const user = await User.findById(decoded.id).select('-password');
if (!user) {
// Token was valid but the user was deleted after it was issued
throw new AppError('User no longer exists', 401);
}
// โโ 4. Check if user changed password after token was issued โโโโโโโโโโโ
// (Optional but recommended for high-security apps)
// if (user.passwordChangedAt && decoded.iat < user.passwordChangedAt.getTime() / 1000) {
// throw new AppError('Password changed โ please log in again', 401);
// }
// โโ 5. Attach user to request โ available in all downstream handlers โโโ
req.user = user;
next(); // proceed to the route handler
} catch (err) {
next(err); // passes TokenExpiredError / JsonWebTokenError / AppError to errorHandler
}
};
module.exports = protect;
Authorization: Bearer <token> HTTP header. The Bearer scheme is specified in RFC 6750 for OAuth 2.0 but is also used universally for JWT auth. Always validate that the header starts with 'Bearer ' (with a space) before splitting โ a malformed header without the prefix should return 401, not a crash from split(' ')[1] returning undefined.NODE_ENV check from Chapter 13.The authorize Middleware โ Role-Based Access Control
// server/src/middleware/authorize.js
// Factory function: returns middleware that allows only specified roles
const AppError = require('../utils/AppError');
const authorize = (...allowedRoles) => {
return (req, res, next) => {
// protect must run before authorize โ req.user must be set
if (!req.user) {
return next(new AppError('Not authenticated', 401));
}
if (!allowedRoles.includes(req.user.role)) {
return next(
new AppError(
`Role '${req.user.role}' is not authorised to access this route`,
403
)
);
}
next(); // user has the required role โ proceed
};
};
module.exports = authorize;
// โโ Usage in routes โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// server/src/routes/admin.js
const protect = require('../middleware/auth');
const authorize = require('../middleware/authorize');
// Any authenticated user
router.get('/me', protect, getMe);
// Only editors and admins
router.post('/posts', protect, authorize('editor', 'admin'), createPost);
// Admin only
router.delete('/users/:id', protect, authorize('admin'), deleteUser);
router.get('/stats', protect, authorize('admin'), getAdminStats);
Owner-Only Access โ Beyond Role Checks
// Some resources are only accessible to their owner (or an admin)
// Check ownership inside the controller โ not in middleware
const updatePost = asyncHandler(async (req, res) => {
const post = await Post.findById(req.params.id);
if (!post) throw new AppError('Post not found', 404);
// Check: is the current user the post owner, or an admin?
const isOwner = post.author.toString() === req.user.id;
const isAdmin = req.user.role === 'admin';
if (!isOwner && !isAdmin) {
throw new AppError('Not authorised to update this post', 403);
}
// Proceed with update
const updates = { title: req.body.title, body: req.body.body };
const updated = await Post.findByIdAndUpdate(req.params.id, { $set: updates },
{ new: true, runValidators: true });
res.json({ success: true, data: updated });
});
Testing Auth Routes with Postman
1. POST /api/auth/login โ get token
Body: { "email": "user@example.com", "password": "Test@1234" }
Response: { "token": "eyJ..." }
2. GET /api/auth/me โ verify token works
Authorization: Bearer eyJ...
Expected: 200 { "data": { "_id": "...", "name": "...", "role": "user" } }
3. GET /api/auth/me โ verify expired/invalid token fails
Authorization: Bearer invalidtoken
Expected: 401 { "message": "Invalid token โ please log in again" }
4. POST /api/posts โ verify protect works
No Authorization header
Expected: 401 { "message": "No token provided โ please log in" }
5. DELETE /api/users/123 โ verify authorize works (as non-admin)
Authorization: Bearer <user token>
Expected: 403 { "message": "Role 'user' is not authorised..." }
Common Mistakes
Mistake 1 โ Not running protect before authorize
โ Wrong โ authorize tries to read req.user which is not set:
router.delete('/users/:id', authorize('admin'), deleteUser);
// authorize runs first โ req.user is undefined โ crash or wrong error
โ Correct โ protect always comes before authorize:
router.delete('/users/:id', protect, authorize('admin'), deleteUser); // โ
Mistake 2 โ Comparing ObjectId and string without .toString()
โ Wrong โ ObjectId !== string even if they represent the same ID:
if (post.author === req.user.id) { ... }
// post.author is a Mongoose ObjectId, req.user.id is a string โ always false!
โ Correct โ convert to string for comparison:
if (post.author.toString() === req.user.id) { ... } // โ
Mistake 3 โ Using 401 instead of 403 for authorisation failures
โ Wrong โ returning 401 when a user is authenticated but lacks permission:
throw new AppError('Not allowed', 401); // 401 = not authenticated
โ Correct โ use the semantically correct status codes:
// 401 Unauthorized = not authenticated (no token or invalid token)
// 403 Forbidden = authenticated but not permitted
throw new AppError('Not authorised to access this route', 403); // โ
Quick Reference
| Task | Code |
|---|---|
| Extract token | req.headers.authorization?.split(' ')[1] |
| Verify and decode | const decoded = jwt.verify(token, JWT_SECRET) |
| Attach user | req.user = await User.findById(decoded.id).select('-password') |
| Protect a route | router.get('/me', protect, getMe) |
| Restrict to roles | router.delete('/x', protect, authorize('admin'), handler) |
| Owner check | post.author.toString() === req.user.id |
| No token โ 401 | AppError('No token provided', 401) |
| Wrong role โ 403 | AppError('Not authorised', 403) |