Skip to main content

Express Role Management

Introduction

Role management is a crucial aspect of building secure web applications. It enables you to control what actions different types of users can perform within your application. For example, you might want administrators to have full access to your application, while regular users have limited access.

In this tutorial, we'll explore how to implement role-based access control (RBAC) in an Express application. We'll build on the authentication concepts you've already learned and add a layer of authorization to determine what authenticated users are allowed to do.

Understanding Role-Based Access Control

Role-Based Access Control (RBAC) is an approach to restricting system access to authorized users based on roles. In this model:

  • Users are assigned particular roles
  • Roles are associated with permissions
  • Permissions allow access to specific resources or operations

This creates a separation that makes managing user privileges more efficient and secure.

Setting Up Your Project

Before we begin implementing role management, make sure you have a basic Express application with authentication already set up. If you don't, you can follow the previous tutorials in the Express Authentication section.

Let's start by installing the required packages:

bash
npm install express mongoose jsonwebtoken bcryptjs

Creating a User Model with Roles

First, we need to modify our User model to include roles:

javascript
const mongoose = require('mongoose');
const bcrypt = require('bcryptjs');

const UserSchema = new mongoose.Schema({
username: {
type: String,
required: true,
unique: true
},
email: {
type: String,
required: true,
unique: true
},
password: {
type: String,
required: true
},
role: {
type: String,
enum: ['user', 'admin', 'editor'],
default: 'user'
},
createdAt: {
type: Date,
default: Date.now
}
});

// Hash password before saving
UserSchema.pre('save', async function(next) {
if (!this.isModified('password')) {
return next();
}

const salt = await bcrypt.genSalt(10);
this.password = await bcrypt.hash(this.password, salt);
next();
});

// Method to compare passwords
UserSchema.methods.matchPassword = async function(enteredPassword) {
return await bcrypt.compare(enteredPassword, this.password);
};

module.exports = mongoose.model('User', UserSchema);

In this model, we've added a role field that can have one of three values: 'user', 'admin', or 'editor'. By default, new users are assigned the 'user' role.

Creating Role-Based Middleware

Next, we'll create middleware functions to check if a user has the required role to access specific routes:

javascript
// middleware/auth.js
const jwt = require('jsonwebtoken');
const User = require('../models/User');

exports.protect = async (req, res, next) => {
let token;

if (req.headers.authorization && req.headers.authorization.startsWith('Bearer')) {
token = req.headers.authorization.split(' ')[1];
}

if (!token) {
return res.status(401).json({ success: false, message: 'Not authorized to access this route' });
}

try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.user = await User.findById(decoded.id);
next();
} catch (err) {
return res.status(401).json({ success: false, message: 'Not authorized to access this route' });
}
};

exports.authorize = (...roles) => {
return (req, res, next) => {
if (!roles.includes(req.user.role)) {
return res.status(403).json({
success: false,
message: `User role ${req.user.role} is not authorized to access this route`
});
}
next();
};
};

Here we've created two middleware functions:

  • protect: Verifies the JWT token and attaches the user to the request object
  • authorize: Takes an array of allowed roles and checks if the authenticated user's role is included

Implementing Role-Based Routes

Now, let's use our middleware to protect routes based on roles:

javascript
const express = require('express');
const router = express.Router();
const { protect, authorize } = require('../middleware/auth');

// Public route - accessible by anyone
router.get('/public-resource', (req, res) => {
res.json({ success: true, message: 'This is a public resource' });
});

// Protected route - accessible by any authenticated user
router.get('/user-resource', protect, (req, res) => {
res.json({ success: true, message: 'This is a protected user resource' });
});

// Admin route - only accessible by admins
router.get('/admin-resource', protect, authorize('admin'), (req, res) => {
res.json({ success: true, message: 'This is an admin resource' });
});

// Editor route - accessible by editors and admins
router.get('/editor-resource', protect, authorize('editor', 'admin'), (req, res) => {
res.json({ success: true, message: 'This is an editor resource' });
});

module.exports = router;

As you can see, we're using the protect middleware to ensure that users are authenticated, and the authorize middleware to check if they have the necessary role.

User Registration and Login with Roles

Now, let's implement user registration and login functionality:

javascript
// controllers/authController.js
const User = require('../models/User');
const jwt = require('jsonwebtoken');

// Generate JWT Token
const generateToken = (id) => {
return jwt.sign({ id }, process.env.JWT_SECRET, {
expiresIn: '30d'
});
};

// Register User
exports.register = async (req, res) => {
try {
const { username, email, password, role } = req.body;

// Create user
const user = await User.create({
username,
email,
password,
// Only allow setting role if specified and user is allowed to do so
// For production apps, you'd want stricter controls here
role: role || 'user'
});

// Generate token
const token = generateToken(user._id);

res.status(201).json({
success: true,
token,
user: {
id: user._id,
username: user.username,
email: user.email,
role: user.role
}
});
} catch (error) {
res.status(400).json({
success: false,
message: error.message
});
}
};

// Login User
exports.login = async (req, res) => {
try {
const { email, password } = req.body;

// Check if email and password are provided
if (!email || !password) {
return res.status(400).json({
success: false,
message: 'Please provide email and password'
});
}

// Check if user exists
const user = await User.findOne({ email });

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

// Check if password matches
const isMatch = await user.matchPassword(password);

if (!isMatch) {
return res.status(401).json({
success: false,
message: 'Invalid credentials'
});
}

// Generate token
const token = generateToken(user._id);

res.status(200).json({
success: true,
token,
user: {
id: user._id,
username: user.username,
email: user.email,
role: user.role
}
});
} catch (error) {
res.status(500).json({
success: false,
message: error.message
});
}
};

Setting Up the Auth Routes

Let's set up the routes for authentication:

javascript
// routes/authRoutes.js
const express = require('express');
const router = express.Router();
const { register, login } = require('../controllers/authController');

router.post('/register', register);
router.post('/login', login);

module.exports = router;

Role Management in Action

Let's see how you can use these concepts in a real-world application. For example, a blog platform where:

  • Regular users can read articles
  • Editors can create and edit articles
  • Admins can manage users and content

Example: Blog Post Routes

javascript
// routes/postRoutes.js
const express = require('express');
const router = express.Router();
const { protect, authorize } = require('../middleware/auth');
const {
getPosts,
getPost,
createPost,
updatePost,
deletePost
} = require('../controllers/postController');

// Public routes
router.get('/', getPosts);
router.get('/:id', getPost);

// Protected routes
router.post('/', protect, authorize('editor', 'admin'), createPost);
router.put('/:id', protect, authorize('editor', 'admin'), updatePost);
router.delete('/:id', protect, authorize('admin'), deletePost);

module.exports = router;

Example: User Management Routes

javascript
// routes/userRoutes.js
const express = require('express');
const router = express.Router();
const { protect, authorize } = require('../middleware/auth');
const {
getUsers,
getUser,
updateUser,
deleteUser,
changeUserRole
} = require('../controllers/userController');

// Admin only routes
router.get('/', protect, authorize('admin'), getUsers);
router.get('/:id', protect, authorize('admin'), getUser);
router.put('/:id', protect, authorize('admin'), updateUser);
router.delete('/:id', protect, authorize('admin'), deleteUser);
router.patch('/:id/role', protect, authorize('admin'), changeUserRole);

module.exports = router;

A More Advanced Approach: Permission-Based Authorization

For more complex applications, a simple role-based system might not be enough. You might want to implement a permission-based system where each role has specific permissions:

javascript
const mongoose = require('mongoose');

const PermissionSchema = new mongoose.Schema({
name: {
type: String,
required: true,
unique: true
},
description: String
});

const RoleSchema = new mongoose.Schema({
name: {
type: String,
required: true,
unique: true
},
permissions: [{
type: mongoose.Schema.Types.ObjectId,
ref: 'Permission'
}],
description: String
});

const UserSchema = new mongoose.Schema({
// ... other user fields
role: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Role',
required: true
}
});

const Permission = mongoose.model('Permission', PermissionSchema);
const Role = mongoose.model('Role', RoleSchema);
const User = mongoose.model('User', UserSchema);

module.exports = { Permission, Role, User };

You would then modify the authorize middleware to check permissions instead of just roles:

javascript
exports.hasPermission = (permissionName) => {
return async (req, res, next) => {
try {
// Populate user's role and its permissions
const user = await User.findById(req.user.id).populate({
path: 'role',
populate: {
path: 'permissions'
}
});

if (!user) {
return res.status(404).json({
success: false,
message: 'User not found'
});
}

// Check if the user's role has the required permission
const hasPermission = user.role.permissions.some(permission => permission.name === permissionName);

if (!hasPermission) {
return res.status(403).json({
success: false,
message: 'You do not have permission to perform this action'
});
}

next();
} catch (error) {
return res.status(500).json({
success: false,
message: error.message
});
}
};
};

Testing Role Management

Let's write a simple script to test our role management functionality:

javascript
// test-roles.js
const axios = require('axios');

const API_URL = 'http://localhost:5000/api';
let adminToken, editorToken, userToken;

async function testRoles() {
try {
// Login as admin
const adminLogin = await axios.post(`${API_URL}/auth/login`, {
email: '[email protected]',
password: 'adminpassword'
});
adminToken = adminLogin.data.token;
console.log('Admin login successful');

// Login as editor
const editorLogin = await axios.post(`${API_URL}/auth/login`, {
email: '[email protected]',
password: 'editorpassword'
});
editorToken = editorLogin.data.token;
console.log('Editor login successful');

// Login as user
const userLogin = await axios.post(`${API_URL}/auth/login`, {
email: '[email protected]',
password: 'userpassword'
});
userToken = userLogin.data.token;
console.log('User login successful');

// Test admin access
const adminResource = await axios.get(`${API_URL}/admin-resource`, {
headers: { Authorization: `Bearer ${adminToken}` }
});
console.log('Admin can access admin resource:', adminResource.data);

// Test editor access to editor resource
const editorResource = await axios.get(`${API_URL}/editor-resource`, {
headers: { Authorization: `Bearer ${editorToken}` }
});
console.log('Editor can access editor resource:', editorResource.data);

// Test admin access to editor resource
const adminToEditorResource = await axios.get(`${API_URL}/editor-resource`, {
headers: { Authorization: `Bearer ${adminToken}` }
});
console.log('Admin can access editor resource:', adminToEditorResource.data);

// Test user access to user resource
const userResource = await axios.get(`${API_URL}/user-resource`, {
headers: { Authorization: `Bearer ${userToken}` }
});
console.log('User can access user resource:', userResource.data);

// Test user access to admin resource (should fail)
try {
await axios.get(`${API_URL}/admin-resource`, {
headers: { Authorization: `Bearer ${userToken}` }
});
console.log('ERROR: User should not be able to access admin resource');
} catch (error) {
console.log('CORRECT: User cannot access admin resource');
}

} catch (error) {
console.error('Error in test:', error.message);
if (error.response) {
console.error('Response data:', error.response.data);
}
}
}

testRoles();

Summary

In this tutorial, we've covered the implementation of role-based access control in an Express application. We've learned:

  1. How to design a user model with roles
  2. How to create middleware for protecting routes based on roles
  3. How to implement authentication with JWT
  4. How to handle role-based access to different routes
  5. A more advanced permission-based authorization approach

Role management is essential for creating secure applications that provide different levels of access to different types of users. By implementing RBAC, you can ensure that users can only access the parts of your application that they should be allowed to use.

Additional Resources

Exercises

  1. Implement the permission-based authorization system described in the tutorial.
  2. Add additional roles to the system (e.g., 'moderator', 'subscriber').
  3. Create a user management interface that allows admins to change user roles.
  4. Implement route-specific permissions, where users can be granted access to specific resources.
  5. Add logging for all authorization failures to help identify potential security incidents.


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