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.
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?”
In Express.js, authentication is often handled first, and once the user is authenticated, authorization middleware is used to enforce access control.
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.
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.
Let’s start by creating a simple role-based authorization middleware.
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}`);
});
/public
is accessible to everyone./admin
is accessible only to users with the “admin” role./user
is accessible only to users with the “user” role.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.
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.
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}`);
});
/dashboard
is accessible to both “admin” and “user” roles.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
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.
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');
});
/settings
is protected and only accessible to users with the “admin” role./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
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.
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.
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');
});
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) 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.
Let’s implement a simple ABAC system where access is granted based on the user’s department.
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');
});
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
Grant users the minimum permissions they need to perform their tasks. This reduces the risk of unauthorized access or accidental changes.
Regularly audit roles, permissions, and access controls to ensure they align with current business requirements and security policies.
Use authorization middleware to centralize and reuse authorization logic. Avoid scattering authorization checks throughout your application code.
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 !❤️