How to Implement Role-Based Access Control
Implement RBAC by modeling permissions into roles, carrying roles in token claims, enforcing them through a reusable middleware guard, and centralizing the policy. Includes 401-vs-403 distinction and testing.
What and why
Authentication answers who you are; authorization answers what you may do. Role-Based Access Control (RBAC) groups permissions into roles and assigns roles to users, so access rules stay manageable as the system grows. Centralizing the checks prevents inconsistent, ad-hoc rules scattered across handlers.
Prerequisites
- An API with authentication in place.
- A user store that records roles.
- Familiarity with middleware.
Steps
1. Model roles and permissions
Define permissions as fine-grained verbs on resources, then compose them into roles:
const permissions = { admin: ['article:*'], editor: ['article:read', 'article:write'], viewer: ['article:read'] };
Granting permissions through roles, rather than to individual users, keeps assignment simple.
2. Attach roles to the identity
Carry roles in the verified token claims (for example a roles claim in a JWT or an OIDC token) so each request already knows the caller's roles without an extra lookup.
3. Enforce with middleware
Write a reusable guard that checks a required permission:
const require = (perm) => (req, res, next) => {
const granted = req.user.roles.flatMap(r => permissions[r] || []);
const ok = granted.some(p => p === perm || p === perm.split(':')[0] + ':*');
return ok ? next() : res.status(403).json({ error: 'forbidden' });
};
app.post('/v1/articles', require('article:write'), createArticle);
Return 403 for an authenticated user lacking permission, distinct from 401 for no identity.
4. Centralize the policy
Keep the permission map and the guard in one module so every route uses the same logic. For complex rules (ownership, attributes), consider a policy engine such as OPA so policy lives outside scattered code.
5. Test authorization rules
Write tests for each role against each protected route, including the negative cases. Authorization bugs are usually missing checks, so test that forbidden actions actually return 403.
Verification
- An admin can perform all actions.
- A viewer is denied write actions with 403.
- An unauthenticated request returns 401, not 403.
Next Steps
Add resource-level checks for ownership (a user may edit only their own records), audit-log authorization decisions, and move toward attribute-based access control if role explosion becomes a problem.
Prerequisites
- An authenticated API
- A user store with roles
- Basic middleware knowledge
Steps
- 1Model roles and permissions
- 2Attach roles to the identity
- 3Enforce with middleware
- 4Centralize the policy
- 5Test authorization rules