Post

Authentication vs Authorization: Deep Dive

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.

This post is licensed under CC BY 4.0 by the author.