RBAC Authorization
Rules
- Check permissions on EVERY route with middleware — never make authorization optional
- Deny by default: if no explicit permission is granted, access is denied
- Server-side enforcement: hiding UI elements is NOT security, check on the server
- Define role hierarchy: admin > editor > viewer — higher roles inherit lower permissions
- Resource-level checks: verify
can(user, "edit", resource) — not just role-based access
- Audit all permission checks — log who accessed what and when, especially for admin actions
- Separate authentication (who are you?) from authorization (what can you do?)
type Action = "read" | "create" | "update" | "delete";
type Resource = { ownerId: string; [key: string]: any };
function can(user: { id: string; role: string }, action: Action, resource?: Resource): boolean {
const permissions: Record<string, Action[]> = {
admin: ["read", "create", "update", "delete"],
editor: ["read", "create", "update"],
viewer: ["read"],
};
const allowed = permissions[user.role];
if (!allowed?.includes(action)) return false;
if (resource && action !== "read" && user.role !== "admin") {
return resource.ownerId === user.id;
}
return true;
}
function requirePermission(action: Action) {
return (req, res, next) => {
if (!req.user) return res.status(401).json({ error: "Unauthenticated" });
if (!can(req.user, action, req.resource)) {
return res.status(403).json({ error: "Forbidden" });
}
next();
};
}
app.put("/api/posts/:id", loadResource, requirePermission("update"), updatePost);
Avoid
- Checking permissions only in the UI — attackers call your API directly
- Allowing access unless explicitly denied — always deny by default
- Using a single
isAdmin boolean — roles should be granular and extensible
- Forgetting resource-level checks — "editor" should not mean "can edit everyone's content"
- Skipping authorization on "internal" endpoints — they become public eventually