Skip to course content
Free FastAPI backend course

FastAPI for AI Backend Development

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.