API Authorization
Determining what authenticated identities are permitted to do — RBAC, ABAC, and ReBAC models, JWT claims-based authorization, Open Policy Agent (OPA), OAuth2 scopes, resource-based authorization, and field-level response filtering.
What it is
API authorisation (authz) determines whether an authenticated identity is permitted to perform a requested action on a specific resource. Where authentication answers "who are you?", authorisation answers "what can you do?" The distinction matters architecturally: authentication happens at the edge (API gateway); authorisation may need to happen at the service level where domain context is available.
The three dominant authorisation models for APIs are:
- RBAC (Role-Based Access Control) — permissions are assigned to roles; identities are assigned to roles. Simple and well-understood.
- ABAC (Attribute-Based Access Control) — permissions are evaluated based on arbitrary attributes of the subject, resource, action, and environment. Flexible but complex.
- ReBAC (Relationship-Based Access Control) — permissions are derived from the relationships between entities in a graph (e.g., Google Zanzibar). Powerful for social and collaborative systems.
Why it exists
Without authorisation, any authenticated user can access any resource — authentication alone is insufficient. Authorisation enforces the principle of least privilege: each identity gets exactly the access it needs for its role and nothing more. As systems grow in complexity, hardcoded if (user.role == "admin") checks become unmaintainable; externalising authorisation to a policy engine (OPA, Zanzibar) keeps it auditable, testable, and consistent across services.
When to use
- RBAC — simple hierarchical permission structures (admin, editor, viewer).
- ABAC — dynamic permissions based on resource attributes, time, location, or contract terms.
- ReBAC — document sharing, social graphs, or any system where access is determined by ownership chains and group membership.
- OPA — multi-service policy enforcement where policies need to be audited, version-controlled, and tested independently.
When not to use
- Don't centralise all authorisation in the gateway when resource-level checks require domain context not available there.
- Don't use ABAC when RBAC is sufficient — ABAC policies are harder to reason about and audit.
Typical architecture
RBAC (simple):
User → JWT { "roles": ["editor", "billing-viewer"] }
API check: if NOT roles.contains("editor") → 403
ABAC with OPA:
Request (input) → OPA Policy Engine → allow/deny
input = {
"user": { "id": "42", "department": "finance", "clearance": "L3" },
"resource": { "type": "report", "classification": "confidential", "owner_dept": "finance" },
"action": "read",
"env": { "time": "09:30", "ip": "10.0.0.1" }
}
Rego policy:
allow {
input.action == "read"
input.resource.classification == "confidential"
input.user.department == input.resource.owner_dept
input.user.clearance >= "L3"
}
ReBAC (Zanzibar model):
Tuple store:
(document:doc-1, owner, user:alice)
(document:doc-1, viewer, group:engineering)
(group:engineering, member, user:bob)
Check: can bob view doc-1?
→ is (document:doc-1, viewer, user:bob)?
→ expand viewer: {group:engineering}
→ is bob member of engineering? → yes → ALLOW
OAuth2 Scopes (coarse-grained):
Token: { scope: "orders:read invoices:write" }
orders:read → GET /orders, GET /orders/{id}
invoices:write → POST /invoices, PUT /invoices/{id}
Gateway enforces scope → service enforces resource ownership
Pros and cons
Pros
- RBAC is simple to understand, implement, audit, and explain to stakeholders.
- ABAC enables fine-grained, dynamic policies without code changes — policy-as-data.
- OPA decouples policy from service code — policies can be tested, versioned, and deployed independently.
- OAuth2 scopes provide coarse-grained, user-consented authorisation without the server needing to know all permissions upfront.
- ReBAC scales to complex permission graphs that RBAC can't express without role explosion.
Cons
- RBAC suffers from "role explosion" — you eventually need hundreds of roles to express fine-grained policies.
- ABAC policies can become complex to reason about and may have unintended interactions.
- OPA adds a network call (or sidecar overhead) for each authorisation check unless embedded; latency must be budgeted.
- ReBAC tuple stores require careful consistency management — stale permissions may be evaluated.
- JWT claims-based authz may have stale claims if the token TTL is long and roles change.
Implementation notes
Scopes vs roles vs permissions
OAuth2 scopes represent what the token is allowed to do — typically coarse-grained and user-consented (e.g., read:profile, write:orders). They are defined at the application level. Roles represent job functions (admin, editor); a user may have multiple roles. Permissions are fine-grained operations (e.g., order.cancel, user.delete). The best practice is a layered model: gateway enforces scopes, service enforces roles and fine-grained permissions.
Open Policy Agent (OPA)
OPA is a general-purpose policy engine that evaluates policies written in the Rego language. Policies take a JSON input document (the authorisation request context) and return a decision (allow/deny + optional data). OPA can be deployed as: a sidecar (opa process alongside the service), an SDK embedded in the service, or a central policy service (with higher latency). Keep policies testable — OPA provides opa test for unit testing Rego policies. Use OPA bundles to distribute policy versions with the same CI/CD pipeline as code.
Resource-based authorisation
Resource-based authz checks whether the requesting identity owns or has been granted access to the specific resource instance — not just the resource type. Example: user A can read order #42 (because they own it) but not order #99 (owned by user B). This check requires domain context (the resource's owner field) and must happen inside the service, not at the gateway. Common patterns: automatically filter collection queries with WHERE owner_id = $currentUser, and explicitly check ownership before returning individual items. Return 404 Not Found (rather than 403 Forbidden) when the caller should not know the resource exists — to avoid enumeration attacks.
Attribute filtering in responses
Sometimes a caller is allowed to read a resource but should not see all fields — a non-admin seeing a user record should not see ssn, salary, or internal notes. Implement field-level filtering in the service layer based on the caller's role/attributes, not in the API gateway. Document which fields are returned for each role in the OpenAPI spec. Consider separate response schemas per access level rather than conditional field omission — easier to test and document.
Google Zanzibar / ReBAC
Zanzibar is Google's global, consistent authorisation system powering Google Drive, Calendar, and YouTube. It stores tuples (object, relation, subject) and evaluates permission checks by traversing relation graphs. Open-source implementations include OpenFGA, Ory Keto, and Authzed/SpiceDB. Use ReBAC when permissions are defined by structural relationships (sharing, ownership, group membership) rather than flat role assignments.
Common failure modes
- Object-level authorisation bypass (IDOR) — OWASP API1: the API checks if the user can access the resource type but not whether they own the specific object.
GET /invoices/999returns another user's invoice. - Function-level authorisation bypass — OWASP API5: admin-only endpoints are hidden from the UI but not protected server-side; an attacker calls them directly.
- Stale JWT claims — a user's role is downgraded but their JWT (issued with the old role) remains valid for its full TTL; use short TTLs or token introspection for role-sensitive operations.
- Over-permissive scopes — issuing
*:writewhen the client only needsorders:writeviolates least privilege. - Missing resource ownership check in collections —
GET /ordersreturns all orders in the database instead of only the calling user's orders.
Decision checklist
- Is every resource endpoint checking object-level ownership (not just resource-type access)?
- Are admin-only endpoints protected server-side, not just hidden in the UI?
- Are OAuth2 scopes defined at the least-privilege level (narrow, not wildcard)?
- Are field-level access controls applied to sensitive fields in responses?
- Is authorisation logic externalised and testable (OPA policies with unit tests)?
- Are collection endpoints filtered to the calling identity's owned resources?
- Is 404 returned (not 403) when the resource exists but the caller should not know?
- Are role/permission changes reflected in tokens within an acceptable time window (short TTL or revocation)?
Example use cases
- Multi-tenant SaaS — users belong to an organisation; all data queries are automatically scoped to
WHERE org_id = current_org; cross-org access requires an explicit sharing grant stored in a ReBAC tuple store. - Healthcare API — ABAC policy: a clinician can access a patient record if (a) they are assigned to the patient's care team AND (b) the record classification matches their clearance AND (c) the access is within working hours from an approved network.
- Developer platform — OPA policies govern which CI/CD pipelines can deploy to which environments; policies are stored in Git alongside application code; changes require a PR with peer review and automated Rego unit tests.
Related patterns
- API Authentication — authentication must precede authorisation.
- API Gateway — coarse-grained scope enforcement at the gateway layer.
- OpenAPI Specification — document security schemes and required scopes per endpoint.
Further reading
- OWASP API Security Top 10 — API1 (Broken Object Level Authorization) and API5 (Broken Function Level Authorization).
- Google Zanzibar: Google's Consistent, Global Authorization System (USENIX ATC 2019).
- Open Policy Agent documentation — openpolicyagent.org/docs
- OpenFGA (Fine-Grained Authorization) — openfga.dev
- NIST SP 800-162 — Guide to Attribute Based Access Control (ABAC).