Enforce permissions on the backend (source of truth) and reflect them on the frontend for UX — store roles/permissions in the DB, check them in middleware for every protected action, and keep a simple, auditable mapping so admins can manage access.
Below is a practical, production-minded plan with examples.
1) Decide your model
- Roles → named groupings like
admin,manager,user. - Permissions → fine-grained actions like
user:read,user:update,user:delete. - Role → Permission mapping so roles are collections of permissions.
(Optionally) support user-specific overrides or resource-level permissions (RBAC + ABAC hybrid).
DB schema (simplified):
users: { id, name, email, passwordHash, roleId }
roles: { id, name }
permissions: { id, name } // e.g. "user.delete"
role_permissions: { roleId, permissionId }
2) Enforce server-side checks (always authoritative)
- Always validate permissions in backend routes/controllers — never rely solely on frontend.
- Use middleware to decode the user (JWT/session) and check required permissions before handler runs.
Express middleware example (permission-based):
// auth middleware decodes JWT and sets req.user = { id, role, permissions: [...] }
function requirePermission(permission) {
return (req, res, next) => {
const user = req.user;
if (!user) return res.status(401).json({ message: "Unauthenticated" });
if (user.permissions && user.permissions.includes(permission)) return next();
return res.status(403).json({ message: "Forbidden" });
};
}
// route
app.delete('/users/:id', requireAuth, requirePermission('user.delete'), async (req, res) => {
// safe to delete
});
Notes:
requireAuthshould verify JWT, check expiry, and load user role/permission list (from token or DB/cache).- For critical apps, re-check permissions against DB for sensitive actions or short-lived tokens.
3) How to supply permissions to backend checks
- Option A — include minimal role/perm info in JWT (fast, offline checks). Add
iat,exp, and aversion/rolesHashto allow revocation. - Option B — fetch permissions from DB/cache on each request (always up-to-date). Use Redis to cache role→permission mapping for speed.
Tradeoff: JWT read is faster; DB/cache ensures immediate role changes take effect.
4) Frontend: show/hide UI but do not rely on it for security
- Use a central Auth provider or hook that exposes
userandpermissions. - Hide or disable UI controls based on permissions to improve UX (e.g., hide “Delete” button).
- Protect routes with a client-side guard to improve flow (redirect to 403/unauthorized pages), but remember backend must enforce checks.
React PrivateRoute (permission-aware):
function PrivateRoute({ children, permission }) {
const { user } = useAuth(); // user.permissions array
if (!user) return <Navigate to="/login" replace />;
if (permission && !user.permissions.includes(permission)) return <Navigate to="/unauthorized" replace />;
return children;
}
5) Admin UI & Management
- Provide UI to create/edit roles, assign permissions, and assign roles to users.
- Logs/audit trail for role/permission changes and sensitive operations (who granted what and when).
6) Revocation & token lifecycle
- If using JWTs, handle revocation:
- Use short-lived access tokens + refresh tokens stored securely (httpOnly cookies preferred).
- Keep a token
versionorrolesVersionon the user record — when roles change, increment the version so old tokens are rejected or force refresh. - Optionally maintain a server-side blacklist for emergency revocation.
7) Performance & caching
- Cache role→permission mapping in Redis. Invalidate cache on role/permission updates.
- For high QPS, store permissions in token and revalidate periodically rather than hitting DB on every request.
8) Testing & audit
- Unit tests for middleware (allow/deny paths).
- Integration tests for routes (simulate different roles).
- Log every authorization failure (403) with enough context to debug.
9) Security best practices & pitfalls
- Never trust frontend checks for security.
- Use principle of least privilege — give minimum permissions needed.
- Avoid mapping UI-only flags to security controls — permissions should be action-based.
- Watch for privilege escalation paths (e.g., mass-update endpoints) and protect them specially.
- Sanitize inputs and verify resource ownership for actions that operate on a specific resource (e.g.,
user.updateshould also ensurereq.user.id === targetIdor that user has admin scope).
10) Example: full flow for "delete user"
- Client clicks "Delete" (UI shows button only if permission exists).
- Client calls
DELETE /api/users/:idwith Authorization header. - Server middleware verifies JWT, loads
req.user.permissions(from token or cache). requirePermission('user.delete')allows or rejects.- If allowed, controller deletes user and logs action; else 403 returned.
When to use ABAC or PBAC instead
If you need context-aware rules (time, resource attributes, multi-tenant scoping), consider:
- ABAC (attribute-based) — permissions depend on attributes.
- PBAC (policy-based) — external policy engine (e.g., OPA) for complex rules.
Final checklist
- [ ] Roles + permissions model defined
- [ ] Backend middleware enforces checks for every protected route
- [ ] Admin UI to manage roles/permissions + audit logs
- [ ] Token strategy with revocation plan (short-lived access tokens + refresh)
- [ ] Frontend reads permissions for UX, but never for security
- [ ] Tests for allow/deny cases and monitoring for 403 spikes