Skip to main content

Express Authorization

Introduction

Authorization is the process of determining whether a user has permission to access a specific resource or perform a particular action. While authentication verifies who the user is, authorization decides what that user can do within your application.

In Express applications, authorization typically follows authentication and involves checking user roles, permissions, or other attributes before allowing access to protected routes or resources. Implementing proper authorization is crucial for maintaining security and ensuring users can only access the parts of your application they should be allowed to use.

Authentication vs. Authorization

Before diving deeper, let's clarify the difference:

  • Authentication is about verifying identity ("Who are you?")
  • Authorization is about verifying permissions ("What are you allowed to do?")

Think of it like entering a concert venue:

  • Authentication is checking your ticket at the entrance (confirming you're a valid attendee)
  • Authorization is determining if you have VIP access, backstage passes, etc.

Basic Authorization Concepts

Role-Based Access Control (RBAC)

RBAC is one of the most common authorization models, where permissions are associated with roles, and users are assigned to one or more roles.

Common roles include:

  • Admin: Full access to all features
  • Editor: Can create and modify content but can't manage users
  • User: Basic access to features
  • Guest: Limited access to public content

Permission-Based Authorization

Instead of broad roles, you can implement more granular permissions:

  • read:posts
  • create:posts
  • delete:users
  • manage:settings

Implementing Basic Authorization in Express

Let's start with a simple role-based middleware:

javascript
// Middleware to check if user has required role
const requireRole = (role) => {
return (req, res, next) => {
// Assuming user information is stored in req.user after authentication
if (!req.user) {
return res.status(401).json({ message: 'Unauthorized: No user logged in' });
}

if (req.user.role !== role) {
return res.status(403).json({
message: `Forbidden: Requires ${role} role`
});
}

next();
};
};

// Using the middleware
app.get('/admin/dashboard', requireRole('admin'), (req, res) => {
res.send('Admin Dashboard');
});

app.get('/editor/content', requireRole('editor'), (req, res) => {
res.send('Editor Content Management');
});

Multi-Role Authorization

Often users might have multiple roles. Let's enhance our middleware:

javascript
// Check if user has any of the required roles
const requireAnyRole = (roles) => {
return (req, res, next) => {
if (!req.user) {
return res.status(401).json({ message: 'Unauthorized: No user logged in' });
}

// Check if user's role is in the allowed roles array
if (!roles.includes(req.user.role)) {
return res.status(403).json({
message: `Forbidden: Requires one of these roles: ${roles.join(', ')}`
});
}

next();
};
};

// Using the multi-role middleware
app.get('/reports', requireAnyRole(['admin', 'manager']), (req, res) => {
res.send('Reports Data');
});

Permission-Based Authorization

For more granular control, you might want to check specific permissions instead of roles:

javascript
const checkPermission = (permission) => {
return (req, res, next) => {
if (!req.user) {
return res.status(401).json({ message: 'Unauthorized: No user logged in' });
}

// Assuming user has permissions array
if (!req.user.permissions || !req.user.permissions.includes(permission)) {
return res.status(403).json({
message: `Forbidden: Missing required permission: ${permission}`
});
}

next();
};
};

// Using permission middleware
app.delete('/posts/:id', checkPermission('delete:posts'), (req, res) => {
// Delete post logic
res.send('Post deleted');
});

Real-World Authorization Example

Let's build a more complete example with a user management system:

javascript
const express = require('express');
const session = require('express-session');
const app = express();

// Simplified user database
const users = [
{ id: 1, username: 'admin', password: 'admin123', role: 'admin', permissions: ['read:any', 'write:any', 'delete:any'] },
{ id: 2, username: 'editor', password: 'editor123', role: 'editor', permissions: ['read:any', 'write:own'] },
{ id: 3, username: 'user', password: 'user123', role: 'user', permissions: ['read:own'] }
];

// Middleware
app.use(express.json());
app.use(session({
secret: 'your-secret-key',
resave: false,
saveUninitialized: false
}));

// Authentication middleware (simplified)
app.post('/login', (req, res) => {
const { username, password } = req.body;
const user = users.find(u => u.username === username && u.password === password);

if (!user) {
return res.status(401).json({ message: 'Invalid credentials' });
}

// Store user in session (minus the password)
const { password: _, ...userWithoutPassword } = user;
req.session.user = userWithoutPassword;

res.json({ message: 'Login successful', user: userWithoutPassword });
});

// Authorization middleware
const isAuthenticated = (req, res, next) => {
if (!req.session.user) {
return res.status(401).json({ message: 'Unauthorized: Please login' });
}
next();
};

const hasRole = (role) => {
return (req, res, next) => {
if (req.session.user.role !== role) {
return res.status(403).json({ message: `Forbidden: Requires ${role} role` });
}
next();
};
};

const hasPermission = (permission) => {
return (req, res, next) => {
if (!req.session.user.permissions.includes(permission)) {
return res.status(403).json({ message: `Forbidden: Required permission: ${permission}` });
}
next();
};
};

// Protected routes
app.get('/admin-panel', isAuthenticated, hasRole('admin'), (req, res) => {
res.json({ message: 'Welcome to Admin Panel', user: req.session.user });
});

app.get('/posts', isAuthenticated, hasPermission('read:any'), (req, res) => {
res.json({ message: 'All posts data', posts: [
{ id: 1, title: 'Post 1', content: 'Content 1' },
{ id: 2, title: 'Post 2', content: 'Content 2' }
]});
});

app.post('/posts', isAuthenticated, hasPermission('write:any'), (req, res) => {
res.json({ message: 'Post created successfully' });
});

app.listen(3000, () => {
console.log('Server running on port 3000');
});

Example Output

When an admin user logs in and accesses protected routes:

// POST /login with { "username": "admin", "password": "admin123" }
{
"message": "Login successful",
"user": {
"id": 1,
"username": "admin",
"role": "admin",
"permissions": ["read:any", "write:any", "delete:any"]
}
}

// GET /admin-panel
{
"message": "Welcome to Admin Panel",
"user": {
"id": 1,
"username": "admin",
"role": "admin",
"permissions": ["read:any", "write:any", "delete:any"]
}
}

When a regular user tries to access admin routes:

// POST /login with { "username": "user", "password": "user123" }
{
"message": "Login successful",
"user": {
"id": 3,
"username": "user",
"role": "user",
"permissions": ["read:own"]
}
}

// GET /admin-panel
{
"message": "Forbidden: Requires admin role"
}

Advanced Authorization Techniques

Dynamic Resource-Based Authorization

Sometimes authorization depends not just on the user's role, but also on the resource being accessed. For example, a user might be allowed to edit their own posts but not others'.

javascript
const canEditPost = (req, res, next) => {
const postId = parseInt(req.params.id);
const userId = req.session.user.id;

// Check if admin (can edit any post)
if (req.session.user.role === 'admin') {
return next();
}

// Find the post
const post = posts.find(p => p.id === postId);

if (!post) {
return res.status(404).json({ message: 'Post not found' });
}

// Check if user is the author
if (post.authorId !== userId) {
return res.status(403).json({ message: 'Forbidden: You can only edit your own posts' });
}

next();
};

// Usage
app.put('/posts/:id', isAuthenticated, canEditPost, (req, res) => {
// Update post logic
res.json({ message: 'Post updated successfully' });
});

Using Third-Party Libraries

For complex applications, consider using authorization libraries like CASL, AccessControl, or RBAC:

Example using CASL:

javascript
const { AbilityBuilder, Ability } = require('@casl/ability');

function defineAbilitiesFor(user) {
const { can, cannot, build } = new AbilityBuilder(Ability);

if (user.role === 'admin') {
can('manage', 'all'); // Admin can do anything
} else if (user.role === 'editor') {
can('read', 'all');
can('create', 'Post');
can('update', 'Post');
cannot('delete', 'Post');
} else {
can('read', 'Post');
can('create', 'Comment');
can('update', 'Comment', { authorId: user.id }); // Can only update own comments
}

return build();
}

// Middleware to define user abilities
app.use((req, res, next) => {
if (req.session.user) {
req.ability = defineAbilitiesFor(req.session.user);
}
next();
});

// Authorization middleware using CASL
function checkAbility(action, subject) {
return (req, res, next) => {
if (!req.ability) {
return res.status(401).json({ message: 'Unauthorized' });
}

if (!req.ability.can(action, subject)) {
return res.status(403).json({ message: 'Forbidden' });
}

next();
};
}

// Usage
app.delete('/posts/:id', checkAbility('delete', 'Post'), (req, res) => {
// Delete post logic
res.json({ message: 'Post deleted successfully' });
});

Best Practices for Authorization

  1. Apply the Principle of Least Privilege: Give users only the permissions they need.
  2. Implement Authorization at Multiple Levels:
    • API endpoints
    • Database queries
    • UI elements
  3. Always Verify on the Backend: Never rely solely on frontend authorization.
  4. Use Middleware: Centralize authorization logic in reusable middleware.
  5. Keep Authorization Logic Separate: Separate business logic from authorization rules.
  6. Audit and Log Authorization Decisions: Track access attempts for security reviews.
  7. Use Established Libraries: Don't reinvent the wheel for complex authorization needs.
  8. Regularly Review Authorization Rules: Update as your application evolves.

Summary

Authorization is a critical component of web application security that works alongside authentication to ensure users can only access the resources and perform the actions they're permitted to. In Express.js applications, we can implement authorization through middleware that checks user roles, permissions, or other attributes.

Key approaches include:

  • Role-based authorization for broader access control
  • Permission-based authorization for fine-grained control
  • Resource-based authorization for content ownership rules
  • Using specialized libraries like CASL for complex authorization needs

By implementing proper authorization, you create a more secure application where users can only access what they're supposed to, protecting both your data and your users.

Additional Resources

Exercises

  1. Create a simple Express application with three types of users (admin, editor, and viewer) and implement role-based authorization.
  2. Extend the application to use permission-based authorization with at least five different permissions.
  3. Implement resource-based authorization where users can only edit their own content.
  4. Add CASL or another authorization library to your project and refactor your authorization logic.
  5. Create a user management system where admins can assign roles and permissions to other users.


If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)