Roles and Role Middleware
Objectives
By the end of this lesson, you should be able to:
- Add a role to a user, and include it in a JWT’s claims
- Write middleware that checks a role and rejects the wrong one
- Return the correct status code for an authorization failure
💡 Why this matters: This lesson turns the concept from the last lesson into real, working middleware, the actual mechanism that separates “logged in” from “allowed.”
⚠️ A note on verification: every snippet and every output in this lesson was actually run, with real HTTP requests against a real Express app.
A Role on the User
const users = [
{ id: 1, email: 'erin@example.com', role: 'member' },
{ id: 2, email: 'jordan@example.com', role: 'admin' }
];
A role field is the simplest form of authorization data, other shapes exist (fine-grained permissions, covered briefly in this module’s exercises), but a role covers most real applications’ needs cleanly.
Including the Role in the Token
Module 3’s exercises already added role to the JWT payload at login:
const token = jwt.sign({ userId: user.id, email: user.email, role: user.role }, SECRET, { expiresIn: '1h' });
Since a JWT’s claims are available immediately after verification (Module 3), the role is already sitting on req.user by the time any route handler runs, no extra database lookup needed just to check it.
Authentication Middleware, Unchanged
function requireAuth(req, res, next) {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({ error: 'Unauthorized', message: 'Missing or malformed Authorization header' });
}
try {
req.user = jwt.verify(authHeader.slice(7), SECRET);
next();
} catch (err) {
return res.status(401).json({ error: 'Unauthorized', message: 'Invalid or expired token' });
}
}
Exactly the middleware from Module 3, this module doesn’t replace it, it adds a second layer on top.
Authorization Middleware
function requireRole(role) {
return (req, res, next) => {
if (req.user.role !== role) {
return res.status(403).json({ error: 'Forbidden', message: `Requires ${role} role` });
}
next();
};
}
requireRole is a middleware factory, it returns a middleware function configured for a specific role, a pattern that makes it reusable for any role a route needs to require, requireRole('admin'), requireRole('editor'), and so on, without writing a separate function for each one.
Combining Both
app.get('/api/v1/admin/stats', requireAuth, requireRole('admin'), (req, res) => {
res.json({ totalUsers: users.length });
});
Two middleware functions, in order: requireAuth first (must be logged in at all), requireRole('admin') second (must specifically be an admin). Express runs middleware in the order it’s listed, requireAuth populates req.user, which requireRole then depends on, this order matters, and couldn’t be reversed.
Testing All Three Outcomes
const noAuth = await request(app).get('/api/v1/admin/stats');
console.log(noAuth.status, noAuth.body);
const memberAttempt = await request(app).get('/api/v1/admin/stats').set('Authorization', `Bearer ${memberToken}`);
console.log(memberAttempt.status, memberAttempt.body);
const adminAttempt = await request(app).get('/api/v1/admin/stats').set('Authorization', `Bearer ${adminToken}`);
console.log(adminAttempt.status, adminAttempt.body);
401 { error: 'Unauthorized', message: 'Missing or malformed Authorization header' }
403 { error: 'Forbidden', message: 'Requires admin role' }
200 { totalUsers: 2 }
Three genuinely different outcomes, from the exact same route, based purely on who’s making the request: no token at all, a valid token for the wrong role, and a valid token for the right role.
Try It
- Build
requireRoleand the admin-only route above, and confirm all three outcomes shown. - Add a second protected route restricted to
'member'instead, and confirm an admin’s token is correctly rejected from it too (roles aren’t a hierarchy here,admindoesn’t automatically pass amember-only check, unless the route is written to allow both). - Explain, in your own words, why
requireAuthhas to run beforerequireRolein the middleware chain, not after.
Recap
- A
rolefield on a user, included in the JWT payload, is available onreq.userimmediately after authentication, no extra lookup needed. requireRole(role)is a middleware factory, returning a configured middleware function for a specific role check.- Authentication and authorization middleware compose in order,
requireAuthfirst,requireRolesecond, each responsible for a distinct failure mode.
Next lesson: a more complete example, combining authentication and authorization across several routes with different requirements.