Skip to content

Authorization at the API Gateway

DodaTech Updated 2026-06-28 6 min read

In this tutorial, you'll learn about Authorization at Gateway. We cover key concepts, practical examples, and best practices to help you understand and apply this topic effectively.

Authorization at the gateway determines whether an authenticated client has permission to access a specific resource or perform a specific action.

What You'll Learn

By the end of this lesson, you will implement RBAC, ABAC, scope-based authorization, and IP-based access control at the gateway.

Why It Matters

Authorization logic scattered across services is hard to audit and maintain. Centralizing at the gateway ensures consistent policy enforcement.

Real-World Use

A gateway checks that a user with role "viewer" cannot access DELETE endpoints, while "admin" users have full access. The backend never needs to check permissions.

Authorization Flow

flowchart TD
    Client -->|Request| GW[Gateway]
    GW -->|Authenticate| Auth{Auth OK?}
    Auth -->|No| 401[401 Unauthorized]
    Auth -->|Yes| Authorize{Authorized?}
    Authorize -->|No| 403[403 Forbidden]
    Authorize -->|Yes| Backend[Backend Service]

Role-Based Access Control

# rbac_gateway.py
from typing import Dict, List, Optional, Tuple

class RBACGateway:
    def __init__(self):
        self.role_permissions: Dict[str, List[str]] = {}
        self.route_permissions: Dict[str, str] = {}

    def add_role(self, name: str, permissions: List[str]):
        self.role_permissions[name] = permissions

    def protect_route(self, method: str, path: str, permission: str):
        key = f"{method}:{path}"
        self.route_permissions[key] = permission

    def authorize(self, method: str, path: str, user: Dict) -> Tuple[bool, str]:
        route_key = self._find_matching_route(method, path)
        if not route_key:
            return True, ""

        required_permission = self.route_permissions[route_key]
        user_role = user.get("role", "anonymous")
        user_permissions = self.role_permissions.get(user_role, [])

        if required_permission not in user_permissions:
            return False, f"Role '{user_role}' lacks permission '{required_permission}'"

        return True, ""

    def _find_matching_route(self, method: str, path: str) -> Optional[str]:
        for route_key in self.route_permissions:
            route_method, route_path = route_key.split(":", 1)
            if route_method != method:
                continue
            if self._path_matches(route_path, path):
                return route_key
        return None

    def _path_matches(self, pattern: str, path: str) -> bool:
        pattern_parts = pattern.strip("/").split("/")
        path_parts = path.strip("/").split("/")
        if len(pattern_parts) != len(path_parts):
            return False
        for p, a in zip(pattern_parts, path_parts):
            if p.startswith(":") or p == "*":
                continue
            if p != a:
                return False
        return True

rbac = RBACGateway()
rbac.add_role("admin", ["users:read", "users:write", "users:delete", "orders:read", "orders:write"])
rbac.add_role("editor", ["users:read", "orders:read", "orders:write"])
rbac.add_role("viewer", ["users:read", "orders:read"])

rbac.protect_route("GET", "/api/users", "users:read")
rbac.protect_route("POST", "/api/users", "users:write")
rbac.protect_route("DELETE", "/api/users/:id", "users:delete")
rbac.protect_route("GET", "/api/orders", "orders:read")
rbac.protect_route("POST", "/api/orders", "orders:write")

test_cases = [
    ("GET", "/api/users", {"role": "viewer"}),
    ("DELETE", "/api/users/1", {"role": "viewer"}),
    ("DELETE", "/api/users/1", {"role": "admin"}),
    ("POST", "/api/orders", {"role": "viewer"}),
    ("POST", "/api/orders", {"role": "editor"}),
]

for method, path, user in test_cases:
    allowed, reason = rbac.authorize(method, path, user)
    status = "ALLOWED" if allowed else "DENIED"
    print(f"{method:7s} {path:20s} [{user['role']:8s}] -> {status}")
    if reason:
        print(f"  Reason: {reason}")

Expected output:

GET     /api/users          [viewer ] -> ALLOWED
DELETE  /api/users/1        [viewer ] -> DENIED
  Reason: Role 'viewer' lacks permission 'users:delete'
DELETE  /api/users/1        [admin  ] -> ALLOWED
POST    /api/orders         [viewer ] -> DENIED
  Reason: Role 'viewer' lacks permission 'orders:write'
POST    /api/orders         [editor ] -> ALLOWED

Scope-Based Authorization

# scope_auth.py
from typing import Dict, List, Optional, Tuple

class ScopeAuthorizer:
    def __init__(self):
        self.route_scopes: Dict[str, str] = {}

    def require_scope(self, method: str, path: str, scope: str):
        key = f"{method}:{path}"
        self.route_scopes[key] = scope

    def authorize(self, method: str, path: str, token_scopes: List[str]) -> Tuple[bool, str]:
        for route_key, required_scope in self.route_scopes.items():
            r_method, r_path = route_key.split(":", 1)
            if r_method == method and self._path_matches(r_path, path):
                if required_scope not in token_scopes:
                    return False, f"Required scope: {required_scope}"
                break
        return True, ""

    def _path_matches(self, pattern: str, path: str) -> bool:
        return path.startswith(pattern.rstrip("*"))

authorizer = ScopeAuthorizer()
authorizer.require_scope("GET", "/api/users", "users:read")
authorizer.require_scope("POST", "/api/users", "users:write")
authorizer.require_scope("GET", "/api/admin/*", "admin:access")

tests = [
    ("GET", "/api/users", ["users:read", "profile:read"]),
    ("POST", "/api/users", ["users:read"]),
    ("GET", "/api/admin/settings", ["admin:access"]),
    ("GET", "/api/admin/settings", ["users:read"]),
]

for method, path, scopes in tests:
    allowed, reason = authorizer.authorize(method, path, scopes)
    print(f"{method:7s} {path:25s} scopes={scopes} -> {'ALLOWED' if allowed else f'DENIED ({reason})'}")

Expected output:

GET     /api/users               scopes=['users:read', 'profile:read'] -> ALLOWED
POST    /api/users                scopes=['users:read'] -> DENIED (Required scope: users:write)
GET     /api/admin/settings       scopes=['admin:access'] -> ALLOWED
GET     /api/admin/settings       scopes=['users:read'] -> DENIED (Required scope: admin:access)

Common Mistakes

1. Authorizing After Routing

Authorization must happen after authentication but before request forwarding. Never let unauthorized requests reach backend services.

2. Hardcoding Roles in Application Code

Role-permission mappings should be configurable, not hardcoded. Use a policy engine or configuration file.

3. Not Handling Permission Changes

When user permissions change mid-session, the gateway should re-validate. Use short-lived token caches.

4. Overly Permissive Defaults

Default access should be deny, not allow. Every route must explicitly declare required permissions.

5. Mixing AuthN and AuthZ

Authentication (who you are) and authorization (what you can do) are separate concerns. Handle them in distinct middleware layers.

Practice Questions

1. What is the difference between RBAC and ABAC?

RBAC grants access based on roles. ABAC uses attributes (user, resource, environment) for fine-grained decisions.

2. Why should authorization happen at the gateway?

It provides a single enforcement point, reduces duplication, and makes the security model auditable.

3. How does the gateway return authorization failures?

Return HTTP 403 Forbidden with a clear error message. Never reveal whether the resource exists, to prevent enumeration attacks.

4. What is the principle of Least Privilege in gateway authorization?

Grant only the minimum permissions required for each role or scope. Start with no permissions and add them explicitly.

Challenge

Design an authorization system for a multi-tenant SaaS platform where users have roles per organization, scopes per resource type, and IP-based restrictions for admin endpoints.

FAQ

Can the gateway support fine-grained access control?

Yes. For fine-grained control, pass user attributes to a policy service (OPA, Casbin) that evaluates complex rules.

How does authorization work with API keys?

API keys are associated with a role or scope set. The gateway checks the key's permissions against the route requirements.

Should I cache authorization decisions?

Yes. Cache decisions with a short TTL (1-5 minutes). Invalidate the cache when permissions change.

What is the difference between 401 and 403?

401 means unauthenticated (missing or invalid credentials). 403 means authenticated but not authorized (insufficient permissions).

Can the gateway integrate with external policy engines?

Yes. Gateways like Kong and Envoy integrate with Open Policy Agent (OPA) for externalized policy decisions.

Mini Project: Simple Policy Engine

# policy_engine.py
from typing import Dict, List, Optional, Tuple

class PolicyEngine:
    def __init__(self):
        self.policies: List[Dict] = []

    def add_policy(self, role: str, method: str, path_pattern: str, allow: bool = True):
        self.policies.append({
            "role": role,
            "method": method,
            "path_pattern": path_pattern,
            "allow": allow,
        })

    def evaluate(self, user: Dict, method: str, path: str) -> Tuple[bool, str]:
        role = user.get("role", "anonymous")

        for policy in self.policies:
            if policy["role"] == role and policy["method"] == method:
                if path.startswith(policy["path_pattern"]):
                    if policy["allow"]:
                        return True, "Allowed by policy"
                    return False, f"Denied by policy for role {role}"

        return False, "No matching policy"

engine = PolicyEngine()
engine.add_policy("admin", "GET", "/api/")
engine.add_policy("admin", "POST", "/api/")
engine.add_policy("admin", "DELETE", "/api/")
engine.add_policy("viewer", "GET", "/api/")
engine.add_policy("viewer", "POST", "/api/")

tests = [
    ({"role": "admin"}, "DELETE", "/api/users/1"),
    ({"role": "viewer"}, "DELETE", "/api/users/1"),
    ({"role": "viewer"}, "GET", "/api/users"),
    ({"role": "anonymous"}, "GET", "/api/users"),
]

for user, method, path in tests:
    allowed, reason = engine.evaluate(user, method, path)
    print(f"[{user['role']:10s}] {method:7s} {path:20s} -> {'ALLOWED' if allowed else 'DENIED'}: {reason}")

Expected output:

[admin     ] DELETE /api/users/1        -> ALLOWED: Allowed by policy
[viewer    ] DELETE /api/users/1        -> DENIED: Denied by policy for role viewer
[viewer    ] GET     /api/users          -> ALLOWED: Allowed by policy
[anonymous ] GET     /api/users          -> DENIED: No matching policy

What's Next

You understand authorization patterns. Next, learn about API key management, then explore IP whitelisting.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro