RBAC and ABAC Authorization
RBAC and ABAC Authorization
RBAC (Role-Based Access Control) grants permissions based on the role a user holds; ABAC (Attribute-Based Access Control) grants permissions based on evaluated attributes of the subject, resource, and environment — RBAC is simpler to audit, ABAC is more expressive but harder to reason about.
Core Idea
Two authorization paradigms address the same question — "is this user allowed to perform this action?" — but with fundamentally different models:
RBAC: A user holds one or more roles (admin, editor, viewer). Each role maps to a fixed set of permissions (read:posts, write:posts, delete:users). Authorization checks whether the user's current role set includes the required permission. Simple to implement, simple to audit. Works well when the permission matrix is stable and small.
ABAC: A policy evaluates multiple attributes simultaneously — subject attributes (user.department = "finance"), resource attributes (document.classification = "confidential"), and environment attributes (request.time = "business-hours"). Permissions emerge from policy evaluation rather than a fixed role-permission mapping. Expressive enough to encode arbitrary conditions, but policy complexity grows rapidly and is harder to audit.
JWT access tokens commonly carry roles as claims — for example, "roles": ["admin", "editor"] in the payload. These claims are the typical input to RBAC evaluation. JWT claims are the typical source of role data — always validate the token signature before inspecting claims, per JWT.
When NOT to Use
When NOT to Use RBAC
- Do not use RBAC when permission logic requires evaluating resource-level attributes — if "user can edit this document only if they are the owner" depends on a resource attribute, RBAC cannot express this without an explosion of fine-grained roles
- Do not use RBAC when the permission matrix has hundreds of roles — role proliferation makes auditing impossible; more than ~20 distinct roles signals that ABAC or a hybrid model is needed
- Do not encode resource ownership in role names (e.g.,
document:owner:123) — this makes roles data-coupled and unmanageable at scale
When NOT to Use ABAC
- Do not use pure ABAC for simple, stable permission matrices — ABAC policy files become the "source of truth" for authorization; if there are only 3 roles and 10 permissions, the overhead of a policy engine deployment is not justified
- Do not use ABAC without a policy testing strategy — complex attribute policies are hard to reason about; untested policies silently grant or deny access
- Do not implement ABAC by hand with nested if-else attribute checks — use a dedicated ABAC library (
@casl/ability, OPA) rather than manual attribute evaluation logic
How It Works
RBAC Model
Three components form the RBAC chain:
- Users → Roles: Each user is assigned one or more roles at login time, typically from JWT claims or a database lookup
- Roles → Permissions: Each role maps to a set of permissions (
can:read:posts,can:write:posts) - Authorization check: Does the user's current role set include the required permission?
RBAC vs ABAC Comparison
| Dimension | RBAC | ABAC |
|---|---|---|
| Decision input | User's roles | Subject + resource + environment attributes |
| Permission model | Role → permission mapping | Policy expression evaluated at runtime |
| Expressiveness | Fixed matrix | Arbitrary conditions (ownership, time, department) |
| Audit complexity | Low — trace user → role → permission | High — must trace policy evaluation context |
| Implementation | Library (CASL) or @PreAuthorize annotation | Library (CASL conditions) or policy engine (OPA) |
| When to choose | Stable, small permission matrix | Resource-level conditions, dynamic attributes |
Hybrid RBAC + ABAC Model
Most production systems use RBAC for coarse-grained access (can this user type access this resource type at all?) and ABAC conditions for fine-grained access (is this specific resource instance owned by this user?).
Example: RBAC gate — the user must have the editor role to reach the edit endpoint. ABAC condition — the user can only edit documents where document.authorId === user.sub.
This hybrid model is the recommended approach for most applications — RBAC provides the stable, auditable baseline; ABAC conditions handle ownership and context.
OPA Policy Engine
OPA (Open Policy Agent) is an external, language-agnostic policy engine that separates authorization logic from application code. Policies are written in Rego (OPA's policy language) and evaluated by a sidecar or remote OPA server.
Use OPA when:
- Policy rules must be updated without redeploying the application
- Authorization decisions span multiple services and must be consistent
- Compliance requires auditable policy-as-code (policies live in version-controlled Rego files)
Do NOT use OPA for simple single-service applications where @PreAuthorize or CASL abilities handle the use case — the deployment complexity (sidecar, Rego language, policy testing infrastructure) is not justified.
Similar policy engine patterns are implemented by AWS Cedar, Google Zanzibar, and AuthzED, though each with different policy languages and deployment models.
For most TypeScript/Spring Boot applications, @casl/ability or Spring Security @PreAuthorize covers authorization needs without an external policy engine.
Mermaid Class Diagram
classDiagram
class User {
+String sub
+String[] roles
+String department
}
class Role {
+String name
+Permission[] permissions
}
class Permission {
+String action
+String subject
}
class AbacPolicy {
+String action
+String subject
+evaluate(user, resource) boolean
}
class Resource {
+String id
+String ownerId
+String classification
}
User "1" --> "*" Role : assigned
Role "1" --> "*" Permission : grants
AbacPolicy --> User : evaluates attributes
AbacPolicy --> Resource : evaluates attributes
The RBAC chain runs User → Role → Permission. The ABAC evaluation path shows AbacPolicy examining both User attributes (department, sub) and Resource attributes (ownerId, classification) to compute an authorization decision.
TypeScript Example
// @casl/ability 6.8.0 — isomorphic RBAC/ABAC library for TypeScript
import { AbilityBuilder, createMongoAbility, MongoAbility } from '@casl/ability'
type Actions = 'read' | 'create' | 'update' | 'delete' | 'manage'
type Subjects = 'Post' | 'Comment' | 'User' | 'all'
type AppAbility = MongoAbility<[Actions, Subjects]>
// Define abilities from JWT claims (call after token validation — see [[JWT]])
export function defineAbilityFor(user: { sub: string; roles: string[] }): AppAbility {
const { can, cannot, build } = new AbilityBuilder<AppAbility>(createMongoAbility)
if (user.roles.includes('admin')) {
can('manage', 'all') // RBAC: admin can do everything
} else if (user.roles.includes('editor')) {
can('read', 'Post')
can('create', 'Post')
can('update', 'Post', { authorId: user.sub }) // ABAC condition: own posts only
cannot('delete', 'Post') // explicit deny
} else {
can('read', 'Post') // viewer: read-only
}
return build()
}
// Authorization check at the handler
const ability = defineAbilityFor(jwtPayload)
if (ability.cannot('update', post)) throw new ForbiddenError()CASL conditions ({ authorId: user.sub }) implement the ABAC layer — the editor role is the RBAC gate, the ownership condition is the ABAC refinement. This is the hybrid model.
Java Example
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.stereotype.Service;
// JWT roles are mapped to Spring Security GrantedAuthority by the JWT converter — no manual role extraction needed
@Service
public class PostService {
// RBAC: any user with 'editor' or 'admin' role can read
@PreAuthorize("hasRole('EDITOR') or hasRole('ADMIN')")
public Post getPost(Long postId) { /* ... */ return null; }
// Hybrid RBAC + ABAC: editor role + ownership condition
// SpEL expression accesses the JWT claim via #jwt.subject
@PreAuthorize("hasRole('EDITOR') and #jwt.subject == @postRepository.findById(#postId).orElseThrow().authorId")
public Post updatePost(Long postId, PostRequest request, @AuthenticationPrincipal Jwt jwt) {
// authorization enforced by @PreAuthorize before this line executes
return null;
}
// Admin-only deletion
@PreAuthorize("hasRole('ADMIN')")
public void deletePost(Long postId) { /* ... */ }
}For JWT role claim extraction config in Spring Security, see Token-Relay-Pattern. For the JWT validation that precedes authorization, see JWT.
Related Concepts
| Concept | Relationship |
|---|---|
| JWT | JWT access token claims (roles, sub) are the primary input to RBAC/ABAC evaluation |
| OAuth2-OIDC-Flows | OAuth2 grants produce the access token carrying authorization claims |
| Session-Management | Session context determines how long authorization decisions are cached |