Role-Based Access Control (RBAC)
Role/permission system with middleware, database schema, and route guards
What You’ll Build
After following this guide, you will have a working implementation of role-based access control (rbac) in your project. Implement a flexible role-based access control system. Users are assigned roles (admin, editor, viewer), roles have permissions (create, read, update, delete), and middleware checks permissions before allowing access to routes. Works with any database.
Use Cases & Problems Solved
- Protect routes so only authenticated users can access sensitive pages
- Allow users to sign up, log in, and manage their accounts securely
- Avoid storing raw passwords or building session management from scratch
Prerequisites
- Node.js 18+
- Express.js with auth already set up
- Database (PostgreSQL, MongoDB, etc.)
Step-by-Step Implementation
Define roles and permissions schema
The following snippet shows how to define roles and permissions schema. Copy this into your project and adjust the values for your environment.
-- PostgreSQL schema
CREATE TABLE roles (
id SERIAL PRIMARY KEY,
name VARCHAR(50) UNIQUE NOT NULL -- 'admin', 'editor', 'viewer'
);
CREATE TABLE permissions (
id SERIAL PRIMARY KEY,
action VARCHAR(50) NOT NULL, -- 'create', 'read', 'update', 'delete'
resource VARCHAR(50) NOT NULL -- 'posts', 'users', 'settings'
);
CREATE TABLE role_permissions (
role_id INT REFERENCES roles(id),
permission_id INT REFERENCES permissions(id),
PRIMARY KEY (role_id, permission_id)
);
ALTER TABLE users ADD COLUMN role_id INT REFERENCES roles(id) DEFAULT 3; -- default viewer
Authorization middleware
The following snippet shows how to authorization middleware. Copy this into your project and adjust the values for your environment.
// middleware/authorize.js
function authorize(requiredAction, resource) {
return async (req, res, next) => {
const userRole = await db.query(
`SELECT p.action, p.resource FROM role_permissions rp
JOIN permissions p ON rp.permission_id = p.id
WHERE rp.role_id = $1`, [req.user.role_id]
);
const permissions = userRole.rows;
const hasPermission = permissions.some(
p => p.action === requiredAction && p.resource === resource
);
if (!hasPermission) return res.status(403).json({ error: 'Forbidden' });
next();
};
}
// Usage: router.delete('/posts/:id', authenticate, authorize('delete', 'posts'), deletePost);
⚠️ Don’t Do This
❌ Hardcoding role checks with string comparisons
// Fragile, hard to maintain, easy to miss
if (user.role === 'admin' || user.role === 'editor') {
// allow
}
✅ Use a permission-based system that scales
// Flexible — add new roles without changing code
router.put('/posts/:id',
authenticate,
authorize('update', 'posts'), // checks role_permissions table
updatePost
);
Testing
Add these tests to verify your role-based access control (rbac) implementation works correctly:
// __tests__/rbac.test.ts
import { describe, it, expect } from 'vitest';
describe('Role-Based Access Control (RBAC)', () => {
it('should handle successful authentication', async () => {
// Test the happy path
const result = await authenticate({ email: 'test@example.com', password: 'valid' });
expect(result.user).toBeDefined();
expect(result.error).toBeNull();
});
it('should reject invalid credentials', async () => {
const result = await authenticate({ email: 'test@example.com', password: 'wrong' });
expect(result.user).toBeNull();
expect(result.error).toBeDefined();
});
it('should handle missing fields', async () => {
const result = await authenticate({ email: '', password: '' });
expect(result.error).toBeDefined();
});
});
Verification
# Create roles and permissions in DB, then test:
curl -X DELETE http://localhost:3000/posts/1 \
-H 'Authorization: Bearer VIEWER_TOKEN'
# Should get 403 Forbidden
curl -X DELETE http://localhost:3000/posts/1 \
-H 'Authorization: Bearer ADMIN_TOKEN'
# Should succeed with 200 Related Specs
Secure Session Management with Redis
Express sessions with Redis store, expiration, and fingerprinting
JWT Authentication in Express.js
Access/refresh token pattern with middleware guards and secure cookie storage