Unit 10.02: Authorisation is not authentication
401 means "who are you". 403 means "you, specifically, may not".
Authentication and authorisation are separate checks
A key resolving to a caller with scopes, and a second dependency checking one.
The code calls two endpoints with two keys.
from fastapi import Depends, FastAPI, Header, HTTPException
from fastapi.testclient import TestClient
app = FastAPI()
KEYS = {"key-analyst": {"caller": "analyst", "scopes": {"read"}},
"key-admin": {"caller": "admin", "scopes": {"read", "write"}}}
def caller(x_api_key: str | None = Header(default=None)) -> dict:
identity = KEYS.get(x_api_key or "")
if identity is None:
raise HTTPException(401, {"error": "invalid_api_key"})
return identity
def require(scope: str):
def check(identity: dict = Depends(caller)) -> dict:
if scope not in identity["scopes"]:
raise HTTPException(403, {"error": "forbidden", "needs": scope})
return identity
return check
@app.get("/report", dependencies=[Depends(require("read"))])
def report() -> dict:
return {"ok": True}
@app.delete("/report", dependencies=[Depends(require("write"))])
def delete_report() -> dict:
return {"deleted": True}
client = TestClient(app)
for key, method in [("key-analyst", "get"), ("key-analyst", "delete"),
("key-admin", "delete"), ("bad", "get")]:
r = getattr(client, method)("/report", headers={"X-API-Key": key})
print(f"{key:12} {method.upper():6} -> {r.status_code}")
print("\n401 means 'who are you'; 403 means 'you, specifically, may not'.")
The analyst key reads and cannot delete. That distinction is impossible with a single valid-or-not key check, because a valid key would then authorise everything.
The two status codes tell the caller different things. 401 means try authenticating; 403 means do not bother retrying with the same credential, which is what stops a client looping.
The mistake this prevents
The mistake is one key that grants everything, on the grounds that all callers are internal. The first time a read-only integration needs access, it gets a key that can also delete - and the blast radius of a leak is the whole API.
Takeaway
Authenticate the caller, then authorise the action separately. 401 and 403 tell the client different things and both are needed.
