The Whole Thing: Agent → FastAPI → JWT
The 33-minute capstone, coded live in Colab: the full agent behind a FastAPI /chat endpoint, JWT login with hashed passwords, guarded routes, forged-token and expiry tests — a production-shaped agentic API from an empty notebook.
What you'll learn
- Serve an agent as an async REST API with FastAPI
- Mint, verify and expire JWTs; hash passwords properly
- Guard endpoints with OAuth2 bearer dependencies
- Walk the full login → token → protected /chat pipeline
JWT playground
A JWT is three base64url chunks: header.payload.signature. The decoder below is the notebook's peek(), running live in your browser:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 . eyJzdWIiOiJhbGljZSIsImV4cCI6MTc5OTk5OTk5OX0 . 3vJk4XbF0qPzR8wYtN2mHc5dLuKaGeSAoQx17EnUyfM
peek(header)
{
"alg": "HS256",
"typ": "JWT"
}peek(payload)
{
"sub": "alice",
"exp": 1799999999
}exp = Fri, 15 Jan 2027 07:59:59 GMT
signature
Not data — an HMAC-SHA256 fingerprint of header.payload, made with the server's SECRET_KEY. We can't decode it, and without the key we can't forge it.
We just read the header and payload with no key at all — exactly like the notebook's peek(). The lesson: a JWT is signed, not encrypted. Never put secrets in the payload; the signature only proves who minted it and that nobody changed it.
The guard: five lines that protect every route
oauth2 = OAuth2PasswordBearer(tokenUrl="login") # "tokens arrive in the Authorization header"
def get_current_user(token: str = Depends(oauth2)) -> str:
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) # signature + expiry
except jwt.PyJWTError: # any failure -> 401
raise HTTPException(status.HTTP_401_UNAUTHORIZED, "Could not validate credentials")
return payload["sub"]
@app.post("/chat")
async def chat(req: ChatIn, user: str = Depends(get_current_user)): # <-- PROTECTED
# ^^^^^^^^^^^^^^^^^^^^^^^^^^^ the ENTIRE auth layer.
# FastAPI runs the guard first. If it raises 401, this function never executes.
result = await agent.run(req.message)
return {"user": user, "reply": result.output}One Depends(get_current_user) in the signature and the endpoint is secured — jwt.decode checks the signature and the expiry in a single call. Passwords, meanwhile, are never stored: only SHA-256 hashes live in the DB, and login compares fingerprints.
Walk the pipeline
The capstone ends by attacking its own API. Step through the four requests the notebook fires at the running server:
→ request
requests.post(f"{BASE}/chat", json={"message": "hi"})← response
401 Unauthorized
{"detail": "Not authenticated"}The OAuth2PasswordBearer dependency finds no Authorization header, so FastAPI rejects the request before your /chat code ever runs.
That's the entire course, end to end
A raw JSON-RPC dictionary became an MCP server, grew a brain, learned to delegate, got quantized onto your own hardware — and now sits behind an authenticated production API. The 33-minute capstone video codes this lesson live, from an empty Colab cell.
Go to the course