RBAC is not authorization unless it's enforced server-side
I've reviewed countless applications that claim to have "role-based access control" but actually have something far weaker: role-based UI hiding. This distinction matters because hiding buttons doesn't stop attackers—it just hides the attack surface from honest users.
The Problem
Here's a pattern I see constantly: the frontend checks a user's role and conditionally renders admin components. The developer sees "Delete User" buttons only showing for admins and assumes authorization is handled. But what happens when someone opens DevTools and crafts a direct API request?
// This is NOT authorization
{user.role === 'Admin' && (
<button onClick={() => deleteUser(id)}>Delete User</button>
)} If the API endpoint simply accepts the request without checking the caller's role, you've built a "privilege escalation by cURL" vulnerability. Any authenticated user can call admin endpoints directly.
What Real RBAC Looks Like
Authorization must happen on the server, on every request. Here's the correct pattern:
// Server-side middleware - runs on EVERY request
const requireRole = (...allowedRoles) => {
return (req, res, next) => {
// Extract roles from verified JWT (not from client-supplied data)
const userRoles = req.user?.roles || [];
const hasPermission = allowedRoles.some(role =>
userRoles.includes(role)
);
if (!hasPermission) {
// Log the attempt - this is a security event
auditLog('AUTHZ_DENIED', {
userId: req.user?.id,
attemptedAction: req.method + ' ' + req.path,
requiredRoles: allowedRoles,
actualRoles: userRoles,
});
return res.status(403).json({ error: 'Forbidden' });
}
next();
};
};
// Applied to protected routes
app.delete('/api/users/:id', requireRole('Admin'), deleteUser); Key Principles
- Roles come from the identity provider, not the client. If you're using Azure Entra ID (or any OIDC provider), roles should be included in the access token and validated on the server.
- Check on every request. Middleware or decorators should make it impossible to forget authorization checks.
- Fail closed. If you can't determine the user's role, deny access. Never default to allowing.
- Log denials. Authorization failures are security events. Track them, alert on patterns, and investigate anomalies.
The Frontend's Role
Frontend role checks still have value—they improve user experience by not showing options users can't use. But they're a UX optimization, not a security control. Think of them as "the door looks locked" while server-side checks are "the door is actually locked."
Takeaway
If your authorization logic lives only in the frontend, you don't have authorization—you have suggestions.
Build your RBAC so that even if the frontend is completely compromised or bypassed, the API refuses unauthorized actions. That's the only way to have real role-based access control.