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": []}click Run to see this pane's outputA 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.
What does authentication restrict, that isolation (from the Docker lesson) doesn't?
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.