Authorization middleware

Authorization is a crucial aspect of web application security that ensures users have the right permissions to access specific resources or perform certain actions. While authentication confirms a user's identity, authorization determines what that authenticated user is allowed to do. In Express.js, authorization is often implemented using middleware, which checks the user's roles, permissions, or other attributes before allowing access to particular routes.

What is Authorization?

Authorization is the process of determining whether an authenticated user has the necessary permissions to access a resource or perform an action. It answers the question, “Is this user allowed to do this?”

Authorization vs. Authentication

  • Authentication: Confirms the user’s identity (e.g., “Who are you?”).
  • Authorization: Determines what the user can do (e.g., “What are you allowed to do?”).

In Express.js, authentication is often handled first, and once the user is authenticated, authorization middleware is used to enforce access control.

Why Use Authorization Middleware?

Authorization middleware centralizes the logic for controlling access to routes and resources. By applying authorization checks as middleware, you can enforce consistent access control throughout your application and easily update or extend these checks as needed.

Setting Up Basic Authorization Middleware

Role-Based Access Control (RBAC)

One common approach to authorization is Role-Based Access Control (RBAC), where users are assigned roles, and each role has specific permissions. For example, an “admin” role might have access to all routes, while a “user” role might only have access to basic routes.

Defining Roles and Permissions

Let’s start by creating a simple role-based authorization middleware.

Example 1: Basic Role-Based Authorization Middleware

File: app.js

				
					const express = require('express');
const app = express();
const port = 3000;

// Simulated user data (in a real application, this would come from a database)
const users = [
  { id: 1, username: 'JohnDoe', role: 'admin' },
  { id: 2, username: 'JaneDoe', role: 'user' }
];

// Middleware to simulate user authentication (for example purposes)
function authenticate(req, res, next) {
  const userId = parseInt(req.query.userId); // Assume userId is passed as a query parameter
  req.user = users.find(user => user.id === userId);
  if (req.user) {
    next();
  } else {
    res.status(401).send('User not authenticated');
  }
}

// Authorization middleware to check roles
function authorize(role) {
  return (req, res, next) => {
    if (req.user && req.user.role === role) {
      next();
    } else {
      res.status(403).send('Access forbidden: insufficient privileges');
    }
  };
}

// Public route (accessible to everyone)
app.get('/public', (req, res) => {
  res.send('This is a public route accessible to everyone.');
});

// Admin-only route
app.get('/admin', authenticate, authorize('admin'), (req, res) => {
  res.send('Welcome, Admin. You have access to this route.');
});

// User-only route
app.get('/user', authenticate, authorize('user'), (req, res) => {
  res.send(`Welcome, ${req.user.username}. You have access to this route.`);
});

app.listen(port, () => {
  console.log(`Server running at http://localhost:${port}`);
});

				
			

Explanation:

  • authenticate Middleware: Simulates authentication by looking up a user based on a query parameter. In a real application, this data would come from a session or token.
  • authorize Middleware: Takes a role as an argument and checks if the authenticated user has that role.
  • Public Route: /public is accessible to everyone.
  • Admin Route: /admin is accessible only to users with the “admin” role.
  • User Route: /user is accessible only to users with the “user” role.

Output:

1.Accessing /public returns

				
					This is a public route accessible to everyone.

				
			

2.Accessing /admin?userId=1 (as an admin) returns:

				
					Welcome, Admin. You have access to this route.

				
			

3.Accessing /admin?userId=2 (as a regular user) returns:

				
					Access forbidden: insufficient privileges

				
			

4.Accessing /user?userId=2 (as a regular user) returns:

				
					Welcome, JaneDoe. You have access to this route.

				
			

Handling Multiple Roles and Permissions

Sometimes a user might have multiple roles, or a single role might grant access to multiple resources. We can extend our authorization middleware to handle such scenarios.

Example 2: Authorization Middleware with Multiple Roles

File: app.js

				
					// Authorization middleware to check if the user has one of the allowed roles
function authorize(allowedRoles) {
  return (req, res, next) => {
    if (req.user && allowedRoles.includes(req.user.role)) {
      next();
    } else {
      res.status(403).send('Access forbidden: insufficient privileges');
    }
  };
}

// Admin or User route
app.get('/dashboard', authenticate, authorize(['admin', 'user']), (req, res) => {
  res.send(`Welcome to your dashboard, ${req.user.username}`);
});

				
			

Explanation:

  • authorize Middleware: Takes an array of allowed roles and checks if the authenticated user has one of those roles.
  • Dashboard Route: /dashboard is accessible to both “admin” and “user” roles.

Output:

1.Accessing /dashboard?userId=1 (as an admin) returns

				
					Welcome to your dashboard, JohnDoe

				
			

2.Accessing /dashboard?userId=2 (as a regular user) returns:

				
					Welcome to your dashboard, JaneDoe

				
			

Denying Access by Default

It’s a good practice to deny access by default and only grant access to specific routes based on roles or permissions. This approach helps in securing your application by ensuring that unauthorized access is proactively prevented.

Example 3: Deny by Default

File: app.js

				
					// Deny access by default
app.use((req, res, next) => {
  if (!req.user) {
    return res.status(401).send('User not authenticated');
  }
  next();
});

// Example of protected route
app.get('/settings', authorize(['admin']), (req, res) => {
  res.send('Settings page for admins only');
});

				
			

Explanation:

  • Deny by Default Middleware: This middleware is applied globally to deny access if the user is not authenticated.
  • Protected Route: /settings is protected and only accessible to users with the “admin” role.

Output:

  1. Accessing /settings?userId=1 (as an admin) returns
				
					Settings page for admins only

				
			

2.Accessing /settings?userId=2 (as a regular user) returns:

				
					Access forbidden: insufficient privileges

				
			

Advanced Authorization Techniques

Fine-Grained Permissions

In some applications, roles alone may not be sufficient to control access. You may need to implement fine-grained permissions that specify exactly what actions a user can perform on specific resources.

Defining Permissions

Permissions can be defined at a more granular level, such as “read”, “write”, “delete”, etc. Let’s create a middleware that checks for these permissions.

Example 4: Fine-Grained Permissions Middleware

File: app.js

				
					// Simulated user with roles and permissions
const users = [
  { id: 1, username: 'AdminUser', role: 'admin', permissions: ['read', 'write', 'delete'] },
  { id: 2, username: 'EditorUser', role: 'editor', permissions: ['read', 'write'] },
  { id: 3, username: 'ViewerUser', role: 'viewer', permissions: ['read'] }
];

// Middleware to check if user has the required permission
function checkPermission(requiredPermission) {
  return (req, res, next) => {
    if (req.user && req.user.permissions.includes(requiredPermission)) {
      next();
    } else {
      res.status(403).send('Access forbidden: insufficient permissions');
    }
  };
}

// Route that requires 'read' permission
app.get('/documents', authenticate, checkPermission('read'), (req, res) => {
  res.send('You have access to view the documents');
});

// Route that requires 'write' permission
app.post('/documents', authenticate, checkPermission('write'), (req, res) => {
  res.send('You have access to create or edit documents');
});

// Route that requires 'delete' permission
app.delete('/documents', authenticate, checkPermission('delete'), (req, res) => {
  res.send('You have access to delete documents');
});

				
			

Explanation:

  • checkPermission Middleware: Takes a required permission as an argument and checks if the authenticated user has that permission.
  • Routes: Different routes require different permissions (“read”, “write”, “delete”).

Output:

1.Accessing /documents?userId=3 (as a viewer) returns

				
					You have access to view the documents

				
			

2.Accessing /documents?userId=2 (as an editor) and making a POST request returns:

				
					You have access to create or edit documents

				
			

3.Accessing /documents?userId=2 (as an editor) and making a DELETE request returns:

				
					Access forbidden: insufficient permissions

				
			

Attribute-Based Access Control (ABAC)

Attribute-Based Access Control (ABAC) is a more dynamic and flexible approach where access decisions are based on attributes of the user, the resource, and the environment. For example, you might grant access based on the time of day, the user’s department, or specific conditions.

Implementing ABAC

Let’s implement a simple ABAC system where access is granted based on the user’s department.

Example 5: Attribute-Based Access Control

File: app.js

				
					// Simulated user with attributes
const users = [
  { id: 1, username: 'HRUser', department: 'HR' },
  { id: 2, username: 'ITUser', department: 'IT' }
];

// Middleware to check department
function checkDepartment(requiredDepartment) {
  return (req, res, next) => {
    if (req.user && req.user.department === requiredDepartment) {
      next();
    } else {
      res.status(403).send('Access forbidden: department mismatch');
    }
  };
}

// Route accessible only by HR department
app.get('/hr-documents', authenticate, checkDepartment('HR'), (req, res) => {
  res.send('You have access to HR documents');
});

// Route accessible only by IT department
app.get('/it-documents', authenticate, checkDepartment('IT'), (req, res) => {
  res.send('You have access to IT documents');
});

				
			

Explanation:

  • checkDepartment Middleware: Checks if the user’s department matches the required department.
  • Routes: Different routes are accessible based on the user’s department.

Output:

1.Accessing /hr-documents?userId=1 (as an HR user) returns:

				
					You have access to HR documents

				
			

2.Accessing /it-documents?userId=2 (as an IT user) returns:

				
					You have access to IT documents

				
			

Best Practices for Authorization Middleware

Principle of Least Privilege

Grant users the minimum permissions they need to perform their tasks. This reduces the risk of unauthorized access or accidental changes.

Regularly Review Roles and Permissions

Regularly audit roles, permissions, and access controls to ensure they align with current business requirements and security policies.

Use Middleware Wisely

Use authorization middleware to centralize and reuse authorization logic. Avoid scattering authorization checks throughout your application code.

Secure Sensitive Routes

For highly sensitive routes, consider implementing additional security measures such as multi-factor authentication (MFA) or IP whitelisting.

Authorization is a critical component of web application security, ensuring that users can only access resources and perform actions they are permitted to. Express.js provides flexible middleware options to implement both simple and complex authorization strategies, from role-based access control (RBAC) to fine-grained permissions and attribute-based access control (ABAC).Happy coding !❤️

Table of Contents