Authentication

Restricting who's allowed to call an endpoint

Lesson 8’s isolation motivation restricted what running code can access. Authentication is the analogous concern at the API layer: restricting who’s allowed to call an endpoint in the first place — a real concern the moment an API is reachable by anyone on the network, not just trusted local code.

API key auth with Depends()

The simplest real pattern: a shared secret, checked on every request, implemented as a Depends()-based dependency used purely for its side effect:

from fastapi import Depends, Header, HTTPException

API_KEY = "sk-agent-registry-prod-key"

def verify_api_key(x_api_key: str = Header(default=None)):
    if x_api_key != API_KEY:
        raise HTTPException(status_code=401, detail="invalid or missing API key")

@app.get("/agents", dependencies=[Depends(verify_api_key)])
def list_agents():
    return {"agents": []}
No key vs. the right key
click Run to see this pane's output

A request without a valid X-Api-Key header never reaches list_agents at all — verify_api_key raises before it, exactly the same HTTPException-based rejection covered earlier in this lesson, just now gating access rather than reporting a “not found.” Applying this to every route that needs protection just means adding the same dependencies=[Depends(verify_api_key)] to each one — or, as covered in a later concept, attaching it once to an entire APIRouter instead of repeating it per-route.

This is real, but limited: one shared key means every legitimate caller has equal, undifferentiated access, and revoking access for one caller means changing the key for everyone.

A look at OAuth2 and JWT

The more complete, standard pattern for anything beyond a single shared secret: a login endpoint that verifies real credentials and issues a JWT (JSON Web Token) — a signed, self-contained token a client then sends on every subsequent request instead of a raw password.

from fastapi.security import OAuth2PasswordBearer

oauth2_scheme = OAuth2PasswordBearer(tokenUrl="login")

def get_current_user(token: str = Depends(oauth2_scheme)):
    user = decode_and_verify_jwt(token)   # verifies the signature, extracts the user
    if user is None:
        raise HTTPException(status_code=401, detail="invalid or expired token")
    return user

@app.get("/agents")
def list_agents(user: dict = Depends(get_current_user)):
    return {"agents": [], "requested_by": user["username"]}

A JWT is three base64-encoded parts — header.payload.signature — where the payload holds claims like the username and an expiration time, and the signature (created with a server-side secret) proves the token hasn’t been tampered with. Worth connecting directly back to statelessness, one of REST’s guiding principles from the start of this lesson: the server doesn’t need to remember who’s logged in between requests at all — everything needed to verify a request travels in the token itself, on every request, which is exactly what a stateless API requires.

This course doesn’t implement the full token-issuing flow in depth — libraries like python-jose or PyJWT handle the actual encoding and signature verification — but the shape is worth recognizing: Depends() is still the mechanism doing the work, exactly as in the API key version; only what’s being checked, and how much information it carries, has grown more sophisticated.

Check your understanding
1/5

What does authentication restrict, that isolation (from the Docker lesson) doesn't?

Exercise · Graded (real FastAPI, in-browser)

Implement a verify_api_key dependency checking an X-Api-Key header against a known value ("secret-key-123"), raising HTTPException(status_code=401, detail="invalid or missing API key") when it doesn't match. Apply it to a GET /agents route (returning {"agents": ["research_agent", "support_agent"]} on success) using dependencies=[Depends(verify_api_key)]. Both the dependency and the route must be async def.