Advanced25 min read

Authorization & Roles

Implement role-based access control (RBAC) to restrict what different users can do in your API.

Authentication vs Authorization

These two concepts are frequently confused, but they are fundamentally different.

Authentication answers the question: "WHO are you?" It is the process of verifying a user's identity. When you log in with your email and password, or when your JWT is verified, you are being authenticated. After authentication, the server knows your identity — your user ID, email, and other details from the token.

Authorization answers the question: "WHAT are you allowed to do?" It is the process of determining whether an authenticated user has permission to perform a specific action. A user might be authenticated (we know they are Alice with user ID 42), but that does not mean they can delete other users' accounts or access the admin panel.

These two processes always happen in sequence: authentication first, then authorization. You cannot check what someone is allowed to do if you do not know who they are.

Real-world example: Consider a company's internal application. Every employee can log in (authentication). But only HR staff can view salary information (authorization). Only department managers can approve time-off requests (authorization). Only system administrators can create new user accounts (authorization). The login system is the same for everyone, but what each person can do after logging in depends on their role.

In code, the pattern is clear:

  1. Auth middleware runs first — verifies the JWT, attaches req.user
  2. Role middleware runs second — checks req.user.role against allowed roles
  3. Route handler runs last — only if both checks pass

If authentication fails, return 401 Unauthorized ("we don't know who you are"). If authorization fails, return 403 Forbidden ("we know who you are, but you're not allowed to do this"). These are two distinct HTTP status codes for two distinct problems.

Role-Based Access Control (RBAC)

RBAC is the most common authorization pattern for web applications. The concept is straightforward: assign each user a role, and each role has a defined set of permissions.

Common roles in a typical application:

  • user — The default role. Can read public content, create their own posts, edit and delete their own content, manage their own profile.
  • moderator — Can do everything a user can, plus: review reported content, hide or remove inappropriate posts, ban users temporarily, manage comments.
  • editor — Can do everything a user can, plus: create and edit any content (not just their own), publish and unpublish articles, manage categories and tags.
  • admin — Can do everything. Full access to all resources. Can manage users, roles, settings, and system configuration.

How roles are stored: The user's role is typically a string field in the user document/record in the database: { name: "Alice", email: "alice@example.com", role: "admin" }. When you create a JWT on login, include the role in the payload: jwt.sign({ id: user._id, email: user.email, role: user.role }, secret). This way, every request carries the user's role without an extra database query.

Multiple roles: Some systems let users have multiple roles (stored as an array: roles: ["editor", "moderator"]). This is more flexible but more complex. For most applications, a single role per user is sufficient. If you need more granularity, consider a permission-based system instead of (or in addition to) roles.

Role hierarchy: Roles often form a hierarchy: admin > moderator > editor > user. An admin can do anything a moderator can do, plus more. You can implement this by checking if the user's role is "at least" a certain level, rather than checking for an exact match. This prevents you from having to list every role on every route.

Building Role Middleware

The key insight for role middleware is that it needs to be configurable. Different routes allow different roles. The delete-user route might only allow admins, while the edit-post route might allow both admins and editors. You cannot hardcode a specific role check into the middleware — it needs to accept the allowed roles as parameters.

The solution is a factory function — a function that returns a middleware function. The outer function takes the allowed roles as arguments, and the inner function is the actual middleware that runs on each request:

javascript
function authorize(...roles) {
  return (req, res, next) => {
    if (!req.user) {
      return res.status(401).json({ error: 'Not authenticated' });
    }
    if (!roles.includes(req.user.role)) {
      return res.status(403).json({ error: 'Not authorized' });
    }
    next();
  };
}

The ...roles spread syntax lets you pass any number of role strings. The returned middleware checks if the user's role is in the allowed list. If yes, it calls next(). If no, it returns 403 Forbidden.

Usage with route stacking:

javascript
// Only admins can delete users
router.delete('/users/:id', auth, authorize('admin'), deleteUser);

// Admins and moderators can ban users
router.post('/users/:id/ban', auth, authorize('admin', 'moderator'), banUser);

// Admins, moderators, and editors can create posts
router.post('/posts', auth, authorize('admin', 'moderator', 'editor'), createPost);

// Any authenticated user can view their profile
router.get('/profile', auth, getProfile);

Notice the middleware stack: auth runs first (verifies JWT, sets req.user), then authorize(...) runs (checks the role), and finally the route handler runs. If auth rejects the request, authorize never runs. If authorize rejects the request, the route handler never runs. This layered approach keeps each middleware focused on a single responsibility.

Complete RBAC Implementation

javascript
const express = require('express');
const jwt = require('jsonwebtoken');
const router = express.Router();

// ---- Auth Middleware (from previous lesson) ----
function auth(req, res, next) {
  const authHeader = req.headers.authorization;
  if (!authHeader?.startsWith('Bearer ')) {
    return res.status(401).json({ error: 'No token provided' });
  }
  try {
    req.user = jwt.verify(authHeader.split(' ')[1], process.env.JWT_SECRET);
    next();
  } catch (err) {
    return res.status(401).json({ error: 'Invalid token' });
  }
}

// ---- Role Authorization Middleware (factory function) ----
function authorize(...allowedRoles) {
  return (req, res, next) => {
    // auth middleware must run first to set req.user
    if (!req.user) {
      return res.status(401).json({ error: 'Not authenticated' });
    }

    // Check if user's role is in the allowed roles
    if (!allowedRoles.includes(req.user.role)) {
      return res.status(403).json({
        error: `Access denied. Required role: ${allowedRoles.join(' or ')}`,
      });
    }

    next(); // Role is allowed, continue
  };
}

// ---- Routes with different access levels ----

// Public — no auth needed
router.get('/products', getAllProducts);

// Any authenticated user
router.get('/profile', auth, getProfile);
router.put('/profile', auth, updateProfile);

// Editors and above
router.post('/posts', auth, authorize('admin', 'editor'), createPost);
router.put('/posts/:id', auth, authorize('admin', 'editor'), updatePost);

// Moderators and above
router.delete('/posts/:id', auth, authorize('admin', 'moderator'), deletePost);
router.post('/users/:id/ban', auth, authorize('admin', 'moderator'), banUser);

// Admins only
router.get('/admin/users', auth, authorize('admin'), listAllUsers);
router.delete('/admin/users/:id', auth, authorize('admin'), deleteUser);
router.put('/admin/users/:id/role', auth, authorize('admin'), changeUserRole);

// ---- Ownership check (bonus pattern) ----
function authorizeOwnerOrAdmin(resourceField = 'userId') {
  return async (req, res, next) => {
    // Admins can access anything
    if (req.user.role === 'admin') return next();

    // For others, check if they own the resource
    const resource = await findResourceById(req.params.id);
    if (resource[resourceField] === req.user.id) return next();

    return res.status(403).json({ error: 'Not authorized' });
  };
}

// Users can edit their own posts, admins can edit any post
router.put('/posts/:id', auth, authorizeOwnerOrAdmin('authorId'), updatePost);

Advanced Permissions

Simple role-based checks work well for many applications, but some scenarios require more fine-grained control. Here are advanced patterns used in production systems.

Resource-based permissions go beyond roles to check ownership and relationships. A user should be able to edit their own blog post but not someone else's. The check is not just "is this user an editor?" but "is this user the author of this specific post?"

javascript
async function canEditPost(user, postId) {
  const post = await Post.findById(postId);
  if (user.role === 'admin') return true;      // Admins can edit anything
  if (post.authorId === user.id) return true;  // Authors can edit their own
  return false;                                 // Everyone else cannot
}

Permission matrices define exactly which actions each role can perform on each resource type:

javascript
const permissions = {
  admin:     { users: ['read', 'create', 'update', 'delete'], posts: ['read', 'create', 'update', 'delete'] },
  editor:    { users: ['read'], posts: ['read', 'create', 'update'] },
  user:      { users: ['read'], posts: ['read', 'create'] },
};

This approach scales well because adding a new resource or action only requires updating the matrix, not modifying middleware code.

Feature flags allow you to enable or disable features for specific users, roles, or percentage of users. This is common for gradual rollouts: "10% of users see the new dashboard" or "only beta testers can access the export feature."

Attribute-based access control (ABAC) makes decisions based on attributes of the user, resource, action, and environment. For example: "Users in the EU can only access EU data during business hours." ABAC is the most flexible but also the most complex authorization model.

For most web applications, a combination of role-based checks (for API routes) and ownership checks (for user-specific resources) covers 95% of authorization needs. Start simple and add complexity only when the requirements demand it.

What HTTP status code means 'authenticated but not allowed'?

Ready to practice?

Create your free account to access the interactive code editor, run challenges, and track your progress.