CodingNic

Authorization & Roles

Roles and Role Middleware

Authorization & Roles 15 min read

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

javascript
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:

javascript
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

javascript
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

javascript
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

javascript
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

javascript
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);
text
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

  1. Build requireRole and the admin-only route above, and confirm all three outcomes shown.
  2. 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, admin doesn’t automatically pass a member-only check, unless the route is written to allow both).
  3. Explain, in your own words, why requireAuth has to run before requireRole in the middleware chain, not after.

Recap

  • A role field on a user, included in the JWT payload, is available on req.user immediately 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, requireAuth first, requireRole second, each responsible for a distinct failure mode.

Next lesson: a more complete example, combining authentication and authorization across several routes with different requirements.