Authentication vs Authorization: Deep Dive

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

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.

Contents