Free Ebook cover API Gateways for Beginners: Managing, Securing, and Scaling Web APIs

API Gateways for Beginners: Managing, Securing, and Scaling Web APIs

New course

12 pages

Authorization and Policy Enforcement: Roles, Scopes, and Least Privilege

Capítulo 4

Estimated reading time: 7 minutes

+ Exercise

What Authorization Is (and What It Is Not)

Authorization is the allow/deny decision made after a caller has been identified. The gateway’s job is to apply the same decision rules every time, for every request, based on trusted identity data (typically token claims) and the requested action (route + method). The goal is least privilege: callers only get the minimum permissions needed for their use case.

In practice, authorization at the gateway answers questions like: “Can this user call DELETE /users/{id}?” “Is this client allowed to write?” “Is the caller operating within their tenant?” These decisions should be deterministic, auditable, and consistent across services.

Defining Authorization Checks

RBAC: Role-Based Access Control

RBAC grants access based on roles such as admin, support, or viewer. Roles are usually coarse-grained and map well to administrative endpoints and operational tools.

  • Good fit: admin consoles, internal tools, “break-glass” operations.
  • Risk: roles become too powerful (“admin can do everything”), leading to over-privilege.

Scope-Based Access (OAuth2-style permissions)

Scopes are typically fine-grained permissions such as orders:read or orders:write. They map well to API actions and are often easier to reason about per endpoint.

  • Good fit: public APIs, third-party integrations, separating read vs write.
  • Risk: overbroad scopes (e.g., * or api:full_access) undermine least privilege.

Least Privilege as a Design Rule

Least privilege means you design permissions around specific actions and data boundaries. Two practical patterns are: (1) separate read and write permissions, and (2) enforce data ownership boundaries (for example, tenant isolation) using claims.

Continue in our app.

You can listen to the audiobook with the screen off, receive a free certificate for this course, and also have access to 5,000 other free online courses.

Or continue reading below...
Download App

Download the app

Where Policies Live and How They Are Evaluated

Policies should live in the gateway configuration (or a centralized policy engine integrated with the gateway) so that enforcement is consistent and not re-implemented differently in each service. The gateway evaluates policies per route, typically in this order:

  • Match the route: path + method selects a policy block.
  • Extract trusted attributes: token claims (e.g., sub, roles, scope, tenant_id), plus request context (method, path, headers).
  • Evaluate allow/deny: rules decide whether to forward to the backend.
  • Optionally transform: add derived headers for backends (e.g., X-User-Id) only after verification.

Keep the policy logic close to the route definition so it’s obvious what protects what. Avoid “global allow” rules that accidentally cover sensitive endpoints.

Example 1: Restricting Admin Endpoints (RBAC)

Goal: only users with role admin can access administrative routes.

Step-by-step

  • Step 1: Identify admin endpoints (e.g., /admin/*, /internal/ops/*).
  • Step 2: Decide the required role(s), e.g., admin (and optionally support for read-only admin views).
  • Step 3: Enforce at the gateway per route and deny by default.
  • Step 4: Return a consistent error: 403 Forbidden for authenticated but unauthorized.
# Pseudocode gateway policy (route-level RBAC)  routes:   - path: /admin/*     methods: [GET, POST, PUT, DELETE]     policy:       require_authenticated: true       allow_if:         any:           - claim_contains: { claim: roles, value: admin }       deny_status: 403

Practical tip: if you have multiple admin roles, prefer explicit allow lists (e.g., admin, ops_admin) rather than pattern matching on role names.

Example 2: Read vs Write Scope Separation (Scopes)

Goal: allow read-only clients to fetch data but not modify it. This is one of the simplest and most effective least-privilege wins.

Step-by-step

  • Step 1: Define scopes aligned to actions, e.g., orders:read and orders:write.
  • Step 2: Map HTTP methods to required scopes: GET requires read; POST/PUT/PATCH/DELETE require write.
  • Step 3: Apply per route (or per method group) at the gateway.
# Pseudocode gateway policy (scopes per method)  routes:   - path: /orders     methods: [GET]     policy:       require_authenticated: true       require_scopes: [orders:read]       deny_status: 403   - path: /orders     methods: [POST]     policy:       require_authenticated: true       require_scopes: [orders:write]       deny_status: 403   - path: /orders/*     methods: [PUT, PATCH, DELETE]     policy:       require_authenticated: true       require_scopes: [orders:write]       deny_status: 403

Practical tip: avoid “combined” scopes like orders:read_write. They tend to spread and become the default, defeating the purpose of separation.

Example 3: Tenant-Based Constraints Using Claims

Goal: ensure callers can only access resources within their tenant. This is a data boundary rule, not just a feature permission. The gateway can enforce tenant constraints by comparing a trusted claim (e.g., tenant_id) to a tenant identifier in the request (path, query, or derived from the resource).

Common patterns

  • Tenant in path: /tenants/{tenantId}/invoices
  • Tenant in subdomain/host: {tenant}.api.example.com
  • Tenant in query: ?tenantId=... (usually least preferred because it’s easy to omit or manipulate)

Step-by-step (tenant in path)

  • Step 1: Standardize where tenant identity appears in requests (prefer path or host).
  • Step 2: Ensure the token includes a trusted tenant_id claim (or a list of allowed tenants).
  • Step 3: At the gateway, compare path.tenantId to claims.tenant_id.
  • Step 4: Deny if mismatch, even if the caller has the right feature scope.
# Pseudocode gateway policy (tenant isolation)  routes:   - path: /tenants/{tenantId}/invoices/*     methods: [GET, POST, PUT, DELETE]     policy:       require_authenticated: true       require_scopes: [invoices:read]  # for GET routes; use invoices:write for mutations       allow_if:         all:           - equals: { left: path.tenantId, right: claim.tenant_id }       deny_status: 403

When a user can belong to multiple tenants, model the claim as a list (e.g., tenant_ids) and check membership rather than equality.

Policy Evaluation Per Route: Making It Predictable

Authorization becomes inconsistent when rules are scattered. A practical approach is to define a small set of reusable policy building blocks and attach them per route:

  • require_authenticated: deny if no valid identity context.
  • require_roles: allow if roles intersect with allowed roles.
  • require_scopes: allow if required scopes are present.
  • require_tenant_match: allow if tenant boundary is satisfied.
  • deny_by_default: if no rule matches, deny.

Then, for each route, you compose the minimum set of checks needed. For example: an admin endpoint might be role-only; a customer endpoint might be scope + tenant match; a reporting endpoint might be read scope + additional constraints.

Pitfalls and How to Avoid Them

Trusting Client-Supplied Headers

A common mistake is to accept headers like X-User-Id, X-Role, or X-Tenant-Id from the client and treat them as authoritative. Clients can forge these values.

  • Rule: only trust identity attributes derived from verified tokens or from gateway-side lookups.
  • Gateway practice: strip inbound identity headers from external requests, then add your own headers after verification.
# Pseudocode: remove untrusted headers, then inject trusted ones  policy:   remove_request_headers: [X-User-Id, X-Roles, X-Tenant-Id]   set_request_headers_from_claims:     X-User-Id: claim.sub     X-Tenant-Id: claim.tenant_id

Overbroad Scopes

Scopes like api:*, full_access, or “one scope to rule them all” make it hard to reason about access and easy to leak privileges.

  • Fix: define scopes around resources and actions (resource:read, resource:write).
  • Fix: keep privileged scopes rare and tightly controlled; avoid using them for normal clients.

Inconsistent Policies Between Services

If some services enforce authorization internally while others rely on the gateway, you can end up with gaps: a route might be protected at the gateway but exposed through another path, or a backend might assume the gateway checked something that it didn’t.

  • Fix: decide a clear contract: what the gateway guarantees (e.g., authentication, scopes, tenant match) and what each service must still validate (e.g., object-level ownership, business rules).
  • Fix: standardize policy names and required claims across routes and services.
  • Fix: keep a single source of truth for route-to-policy mapping and review it like code.

Confusing 401 vs 403

  • 401 Unauthorized: caller is not authenticated (missing/invalid identity).
  • 403 Forbidden: caller is authenticated but lacks permission.

Consistent status codes help clients handle errors correctly and help you monitor authorization failures.

Policy Checklist for Every Exposed Endpoint

  • Identity: Does this endpoint require an authenticated caller? If yes, is it enforced at the gateway?
  • Permission model: Is access controlled by roles, scopes, or both? Are required roles/scopes explicitly listed?
  • Least privilege: Are read and write operations separated (different scopes/roles)?
  • Tenant boundary: If multi-tenant, how is tenant determined (path/host)? Is it compared to a trusted claim?
  • Default behavior: If no rule matches, is the request denied by default?
  • Header trust: Are client-supplied identity/tenant headers stripped? Are trusted headers injected only after verification?
  • Consistency: Is the same policy applied across all routes that reach the same backend capability?
  • Error handling: Are 401 and 403 used consistently? Is sensitive detail avoided in error bodies?
  • Auditability: Can you log which policy allowed/denied (without logging secrets) for troubleshooting and reviews?

Now answer the exercise about the content:

In a multi-tenant API, what is the most appropriate gateway authorization check to enforce tenant isolation?

You are right! Congratulations, now go to the next page

You missed! Try again.

Tenant isolation should be enforced by matching a trusted claim (from a verified token) to the tenant in the request (prefer path/host). Client-supplied identity headers can be forged, and overbroad scopes undermine least privilege.

Next chapter

Rate Limiting, Quotas, and Traffic Shaping for API Protection

Arrow Right Icon
Download the app to earn free Certification and listen to the courses in the background, even with the screen off.