Introduction#
Authentication verifies who the caller is, while authorization determines what the caller can do. In distributed systems, confusing these layers leads to over-privileged services and fragile security boundaries.
Authentication Layers#
Authentication can be implemented with different assurance levels.
- Single-factor: Passwords or API keys, suitable for low-risk workloads.
- Multi-factor: Combines passwords with TOTP, WebAuthn, or hardware keys.
- Workload identity: Short-lived certificates or tokens for services.
Authorization Models#
Authorization describes the policy engine and how permissions are expressed.
- RBAC: Role-based access control for coarse permissions.
- ABAC: Attribute-based access control using dynamic context.
- ReBAC: Relationship-based access control for graph-like permissions.
Failure Boundaries#
Separate failure domains for authentication and authorization.
- Authentication failures should never leak sensitive details.
- Authorization failures should be deterministic and auditable.
- Token validation belongs to authentication, while scope checks belong to authorization.
Python Example: FastAPI AuthN vs AuthZ#
The following FastAPI example validates the token and then enforces scopes for authorization.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
from fastapi import Depends, FastAPI, HTTPException
from fastapi.security import OAuth2PasswordBearer
import jwt
app = FastAPI()
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
def authenticate(token: str = Depends(oauth2_scheme)) -> dict:
try:
# Load the RSA public key from your identity provider's JWKS endpoint
# For example, using PyJWKClient:
# jwks_client = jwt.PyJWKClient("https://login.example.com/.well-known/jwks.json")
# signing_key = jwks_client.get_signing_key_from_jwt(token)
# public_key = signing_key.key
return jwt.decode(token, public_key, algorithms=["RS256"], audience="orders-api")
except jwt.PyJWTError as exc:
raise HTTPException(status_code=401, detail="Invalid token") from exc
def authorize(claims: dict, scope: str) -> None:
if scope not in claims.get("scope", "").split():
raise HTTPException(status_code=403, detail="Insufficient scope")
@app.get("/orders")
def list_orders(claims: dict = Depends(authenticate)):
authorize(claims, "orders.read")
return {"status": "ok"}
Designing for Least Privilege#
- Assign scopes at the operation level, not the service level.
- Use step-up authentication for high-risk actions.
- Separate admin APIs from user APIs to avoid privilege bleed.
Conclusion#
Treat authentication and authorization as separate layers with explicit boundaries. This separation makes permissions auditable, reduces blast radius, and improves compliance posture.