Authorization at the API Gateway
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
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