Authorization answers a different question than authentication. Authentication asks "who are you?" — authorization asks "what are you allowed to do?" Most security incidents involve authorization failures, not authentication failures, because authorization is harder to model correctly and easier to get wrong at the edges. Here is how the three dominant models work in 2026.
What changed in 2026
- ReBAC went mainstream. OpenFGA (open-sourced by Okta/Auth0) and Google Zanzibar-inspired systems became accessible enough for mid-size teams, not just hyperscalers.
- OPA (Open Policy Agent) became the standard for infrastructure and Kubernetes authorization; WASM-compiled Rego policies now run at the edge.
- Fine-grained authorization emerged as a product category — platforms like Permit.io, Oso, and Cerbos abstract the storage and evaluation layer so engineers focus on policy, not plumbing.
- JWT claims alone are insufficient — teams realized embedding roles in tokens without a server-side check creates stale authorization; the token might say
admin after a demotion.
The three models
RBAC — Role-Based Access Control
Users are assigned roles; roles have permissions; permissions gate actions on resources.
# Simple RBAC check
def can_do(user_roles: list[str], required_permission: str, permissions_map: dict) -> bool:
for role in user_roles:
if required_permission in permissions_map.get(role, []):
return True
return False
permissions_map = {
"admin": ["post:create", "post:delete", "post:read"],
"editor": ["post:create", "post:read"],
"viewer": ["post:read"],
}
can_do(["editor"], "post:delete", permissions_map) # False
Fits: SaaS applications with a fixed tier structure (free/pro/admin), internal tools, most CMSes.
Breaks down: when permissions depend on the specific resource instance ("edit your own posts but not others") or on context (time, location, risk score).
ABAC — Attribute-Based Access Control
Policy is expressed as conditions over attributes of the user, the resource, the action, and the environment.
ALLOW IF
user.clearance_level >= resource.classification AND
user.department == resource.department AND
env.time BETWEEN 09:00 AND 18:00
Extremely expressive. Used in healthcare (HIPAA-driven row-level data access) and government systems. In 2026, OPA with Rego is the most common ABAC implementation outside bespoke systems.
Fits: compliance-heavy domains, multi-tenant SaaS with complex tenant isolation rules.
Breaks down: when attributes alone cannot capture graph-shaped relationships ("can you see this comment because you're in the workspace that owns the document?").
ReBAC — Relationship-Based Access Control
Permission is derived from the relationship graph between users and objects. Inspired by Google Zanzibar.
# OpenFGA schema fragment
type document
relations
define owner: [user]
define editor: [user] or owner
define viewer: [user] or editor or member from workspace
type workspace
relations
define member: [user]
Checking "can user:alice view document:42?" traverses the graph: Alice → member of workspace:eng → workspace owns document:42 → viewer permission granted.
Fits: collaborative tools (Notion, Google Docs model), folder hierarchies, social graphs, any data where "who can see this?" depends on ownership chains.
Model comparison
| Model |
Expressiveness |
Complexity |
Best for |
| RBAC |
Low |
Low |
Fixed roles, SaaS tiers |
| ABAC |
High |
High |
Contextual, compliance-heavy rules |
| ReBAC |
High (for graphs) |
Medium-High |
Hierarchies, collaborative data |
| ACL (per-resource list) |
High |
Very High |
Rarely; too granular to manage |
How to pick
- New app, unknown future requirements? Start with RBAC. Add resource ownership checks (a lightweight ReBAC pattern) when users need per-resource control.
- Complex tenant isolation or regulatory rules? ABAC via OPA. Write policies as code, version them, test them.
- Documents, folders, workspaces, or social graphs? ReBAC via OpenFGA or a Zanzibar-compatible store.
- Mixed requirements? Layer them: RBAC for coarse-grained role checks, ReBAC for resource-level decisions.
Enforcing correctly
Always enforce at the service layer, not just the UI:
# FastAPI middleware example
@app.get("/posts/{post_id}")
async def get_post(post_id: int, current_user: User = Depends(get_current_user)):
post = db.get(post_id)
if not authz.can(current_user, "read", post):
raise HTTPException(status_code=403, detail="Forbidden")
return post
The check must happen in the API handler or a data-access layer — not only in the frontend. Hiding a button does not prevent a direct API call.
Common mistakes
Embedding mutable roles in JWTs. Tokens are cached. A demoted admin still carries the old role claim until the token expires. Validate roles from the database on sensitive operations, or use short-lived tokens.
Using RBAC when per-resource ownership is needed. Adding can_edit_own_posts as a role permission leads to role explosion. Use a resource ownership check instead.
Policy drift. Authorization logic scattered across route handlers, middleware, and data access. Centralize in a policy module or external engine.
No authorization tests. Test that editor cannot call delete, that cross-tenant reads fail, that demotions take effect. Auth logic bugs are silent until exploited.
Returning 404 instead of 403 for security. Returning 404 for unauthorized resources leaks information about existence. Return 403 Forbidden unless you intentionally want to hide resource existence (valid in some designs).
What to skip
- Rolling a custom policy engine for non-trivial rules — Casbin, OpenFGA, and OPA handle edge cases you will miss.
- Storing permissions entirely in JWTs without a server-side check for high-stakes actions.
- ACL per-resource lists at scale — they become unmanageable past a few thousand resources.
FAQ
What is the difference between authorization and access control?
They are often used interchangeably. "Access control" is the broader concept; "authorization" is the decision step that enforces it.
Should authorization logic live in the database or the application?
Both layers should enforce it. Row-level security in Postgres is a strong safety net; application-layer checks give you better error messages and business logic context.
How do I handle authorization in microservices?
Use a centralized policy decision point (OPA sidecar, or an external authz service) and make each service call it. Avoid duplicating policy logic across services.
Is RBAC enough for a multi-tenant SaaS?
Often yes, if you scope roles per tenant. The common gap is per-resource ownership — add a simple "is this user the owner?" check alongside RBAC and you cover most cases.
Where to go next