Authorization in System Design: RBAC, ABAC, OPA, and Policy Enforcement (Visualized)
Authorization decides what an authenticated principal is allowed to do. This guide covers every major model β ACL, RBAC, ABAC, ReBAC β plus OAuth scopes, policy decision points, Open Policy Agent, and the principle of least privilege, with live animations of each concept.
Authorization is the process by which a system decides whether an authenticated principal β a user, service, or device β is permitted to perform a specific action on a specific resource. It is the answer to the question "What are you allowed to do?" and must not be confused with authentication, which answers the prior question "Who are you?".
Authentication establishes identity β typically by validating a password, a signed JWT, or a client certificate. Authorization then takes that verified identity and checks it against a set of rules to grant or deny access. A system can authenticate you perfectly and still refuse your request: a bank employee authenticates with a smart card, but is not authorized to view every customer account. Getting this separation right is one of the most important correctness properties in any multi-user system.
Policy Decision Point vs Policy Enforcement Point
Modern authorization architectures separate the Policy Enforcement Point (PEP) from the Policy Decision Point (PDP). The PEP is the code closest to the user β an API gateway, a middleware function, a gRPC interceptor β that intercepts every request and asks "Is this allowed?" before passing it on. The PDP is a dedicated service or library that holds the authorization rules and evaluates them, returning allow or deny. Splitting the two lets you update policy in one place, audit every decision centrally, and test rules without touching application code.
The typical request flow is: (1) user sends a request to the PEP; (2) the PEP extracts the principal's identity from the token and packages a structured query β subject, action, resource, context; (3) the PDP evaluates the query against loaded policies and returns a decision; (4) the PEP either forwards the request or returns 403 Forbidden. The PDP can be embedded in-process (e.g., OPA as a Go library) or deployed as a sidecar, trading latency for isolation.
Authorization Models: ACL, RBAC, ABAC, ReBAC
Access Control Lists (ACLs) are the oldest model: a list attached to each resource enumerating which principals can do what. Unix file permissions are ACLs. They scale poorly β adding a user means editing every resource β and they grant no way to reason about why access was given.
Role-Based Access Control (RBAC) introduces an indirection layer: principals are assigned roles (e.g., admin, editor, viewer), and permissions are attached to roles rather than to users. Assigning a new employee the editor role automatically grants all editor permissions. RBAC is the dominant model in enterprise software, Kubernetes, and cloud IAM because it is simple to reason about and audit.
Attribute-Based Access Control (ABAC) evaluates arbitrary attributes of the subject, the resource, the action, and the environment at decision time. A policy might read: allow if user.department == resource.owner_department AND time_of_day is business_hours AND resource.classification != 'SECRET'. ABAC is far more expressive than RBAC β you do not need to pre-create a role for every combination of circumstances β but policies are harder to audit and understand at a glance.
Relationship-Based Access Control (ReBAC), popularized by Google's Zanzibar paper, grants access based on the graph of relationships between entities: a user can edit a document because they are a member of a team that owns the folder containing that document. Google Drive, GitHub, and Airbnb's permissions system use ReBAC. It naturally models real-world ownership hierarchies and is more scalable than ABAC for social/collaborative products.
Comparing the Four Models
No model is universally best. The right choice depends on your scale, team maturity, and how dynamic your permission rules need to be. Here is a side-by-side comparison across the axes that matter most:
| Model | Access based on | Policy location | Expressiveness | Audit ease | Best for |
|---|---|---|---|---|---|
| ACL | Per-resource lists | On the resource | Low | Hard at scale | File systems, small single-resource APIs |
| RBAC | Role assignment | Role-permission mapping | Medium | High β roles are auditable | Enterprises, Kubernetes, cloud IAM |
| ABAC | Subject + resource + env attributes | Central policy rules | Very high | Medium β policies can be complex | Fine-grained multi-tenant SaaS |
| ReBAC | Entity relationship graph | Relationship tuples | High | Medium | Collaborative products (Docs, GitHub) |
Scopes and Claims in Tokens
In OAuth 2.0 and OpenID Connect flows, authorization context travels inside tokens. A claim is a key-value pair inside the token payload asserting something about the principal β sub (subject ID), email, roles, department. A scope is a pre-declared permission string that the client requested and the authorization server approved β read:invoices, write:users, admin:billing.
When the client presents a JWT access token to an API, the PEP validates the signature, checks expiry, then inspects scopes and claims. If the token contains read:invoices but not write:invoices, a POST /invoices request is rejected at the PEP before any business logic runs. This is lightweight, stateless authorization for microservices β no round-trip to a central database per request.
{
"sub": "usr_8f3a",
"email": "alice@example.com",
"roles": ["editor"],
"department": "engineering",
"scope": "read:invoices write:invoices read:users",
"iat": 1712500000,
"exp": 1712503600
}Centralized Policy Engines: Open Policy Agent
Open Policy Agent (OPA) is a general-purpose, open-source policy engine that decouples policy from application code. You write rules in a declarative language called Rego, load them into OPA, and query OPA over a local HTTP API or Go library call. OPA is used to authorize Kubernetes admission, API gateway requests, Terraform plans, and microservice calls β the same policy engine for all layers.
A Rego policy for an invoice API might read: allow if the user's role is editor or admin, or if the user is the owner of the specific invoice being accessed. OPA evaluates the rule against the input document (the query) and the data document (loaded policies and supplementary data such as role assignments). The result is a JSON document β typically {"allow": true} or {"allow": false} β that the PEP reads and enforces.
# Rego snippet (stored in OPA)
package api.authz
default allow = false
allow {
input.user.roles[_] == "admin"
}
allow {
input.user.roles[_] == "editor"
not sensitive_resource
}
sensitive_resource {
input.resource.classification == "SECRET"
}Least Privilege and Deny-by-Default
The principle of least privilege says every principal should be granted only the permissions strictly needed to complete their task β no more. A service that reads from a database should not have write permission. A support agent should not have access to payment data. Enforce this by starting from a deny-by-default posture: if no explicit allow rule matches, the request is rejected. This is the opposite of allow-by-default (allow unless explicitly denied), which is fragile because a missing rule silently grants access rather than blocking it.
In practice, least privilege requires regular access reviews, short-lived credentials (JWT expiry, AWS temporary credentials), and automated de-provisioning when a user changes roles or leaves. Tools like AWS IAM Access Analyzer, Styra DAS for OPA, and Google's Policy Intelligence continuously detect overly permissive grants and flag them for remediation.
Common Authorization Patterns in Microservices
In a microservices architecture, authorization can live at multiple layers simultaneously. The API gateway handles coarse-grained checks β is the token valid? does it carry the right scope? β while individual services handle fine-grained checks β is this user the owner of this record? Pushing all authorization logic to the gateway is tempting but creates a coupling problem; pushing all of it into individual services creates duplication. The pragmatic split: gateway enforces authentication and scope validity; services enforce resource-level rules, delegating to a shared policy engine or library.
| Pattern | Where policy runs | Pros | Cons |
|---|---|---|---|
| Token scopes (JWT) | PEP validates token inline | Stateless, fast, no extra hop | Coarse-grained; hard to revoke mid-expiry |
| Centralized PDP (OPA/Zanzibar) | Dedicated policy service | Single source of truth, auditable | Extra network hop; PDP must be HA |
| Service-local RBAC | Inside each microservice | No network dependency | Policy duplication; inconsistent enforcement |
| Sidecar policy proxy | Sidecar intercepts all traffic | No code changes in service | Complex infrastructure; sidecar latency |
Frequently Asked Questions
What is the difference between authentication and authorization?
Authentication proves identity β you are who you claim to be β using a password, OTP, biometric, or certificate. Authorization happens after authentication and decides what that verified identity is allowed to do. A user can be fully authenticated and still be denied access to a resource they do not have permission for. Getting this sequence right matters: never run authorization logic against an unauthenticated request, and never use authentication credentials directly as authorization tokens without an explicit permission model between them.
When should I use RBAC vs ABAC?
Use RBAC when your permission model maps cleanly to job functions (admin, editor, viewer), when your team needs to audit access at a glance, and when the number of distinct permission patterns is small. Use ABAC when context matters β time of day, resource classification, the user's geographic location, multi-tenancy β and when RBAC would require an explosion of fine-grained roles to model all cases. Many production systems start with RBAC and layer ABAC conditions on top for specific sensitive resources, which is a reasonable middle ground supported by engines like OPA and AWS Cedar.
How do you revoke authorization for a stolen JWT?
JWTs are stateless and self-contained: once issued, they are valid until expiry unless you add server-side state. Common revocation strategies are: (1) short expiry β keep access token lifetime to 5β15 minutes; (2) token blocklist β store revoked JTI (JWT ID) values in Redis and check on each request; (3) refresh token rotation β revoke the refresh token so the attacker cannot get a new access token after expiry; (4) signed logout events β broadcast a revocation event to all services via a message queue. The right approach depends on your security posture and the performance cost you can tolerate for the blocklist lookup.
Authentication tells you who is at the door. Authorization decides whether to let them in β and into which rooms. Get the second part wrong and identity becomes meaningless.
β alokknight Engineering
