← Master Index
Vol. 18 Module 18.2 Lecture

Authentication

Backend & Infrastructure

How This Lesson Fits the Module & Volume

Every route you built—FastAPI chat, SSE, WebSockets, job enqueue—is a paid LLM proxy until it is locked. Authentication answers who is calling; authorization answers what they may spend. Vendor keys (OPENAI_API_KEY) stay on the server. Users get your API keys or JWTs.

This is not OAuth theory for its own sake. It is how you stop a leaked SPA from draining the org wallet, and how Redis rate limits attach to identity. Next: deployment of secrets into real environments.

Learning Objectives

By the end of this lesson, students should be able to:

  • Separate vendor secrets from user credentials (API keys / JWT / OIDC).
  • Protect FastAPI routes with a dependency that returns 401/403.
  • Issue hashed, prefix-visible product API keys for machine clients.
  • Authenticate SSE and WebSocket upgrades, not only JSON POST.
  • Attach quotas per principal via Redis.
  • Avoid the “put sk- in localStorage” anti-pattern.
Definition

Authentication (authn) verifies identity. Authorization (authz) decides permissions (model SKU, token budget, admin). In this module, typical mechanisms are: Bearer JWT (human SPA after OIDC/login), product API keys (other backends), and optionally mTLS internally. OpenAI/Anthropic/Gemini keys are not user auth—they are upstream credentials your service uses after the caller is authenticated.

Credential Map

SecretWho holds itWhere it lives
Vendor LLM keyYour backend onlyK8s Secret / vault / env
User JWTBrowser (memory / httpOnly cookie)Issued by your IdP / auth route
Product API keyPartner serverHashed in your DB; prefix shown once
Redis / DB passwordsYour backendSame secret store as vendor keys

FastAPI Bearer Dependency

# pip install fastapi uvicorn python-jose[cryptography] passlib import os, hashlib, hmac from fastapi import Depends, FastAPI, HTTPException from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer from jose import JWTError, jwt app = FastAPI() bearer = HTTPBearer(auto_error=False) JWT_SECRET = os.environ["JWT_SECRET"] # not OPENAI_API_KEY ALGO = "HS256" def hash_api_key(raw: str) -> str: return hashlib.sha256(raw.encode()).hexdigest() # toy store: prefix -> {hash, tenant, rpm} KEYS = {"sk_live_ab12": {"hash": hash_api_key("sk_live_ab12SECRET"), "tenant": "acme", "rpm": 60}} def principal(creds: HTTPAuthorizationCredentials | None = Depends(bearer)) -> dict: if creds is None or creds.scheme.lower() != "bearer": raise HTTPException(status_code=401, detail="missing_bearer") token = creds.credentials if token.startswith("sk_live_"): prefix, _, secret = token.partition("SECRET") # demo only — store prefix+hash properly rec = KEYS.get(token[:12]) if not rec or not hmac.compare_digest(rec["hash"], hash_api_key(token)): raise HTTPException(status_code=401, detail="bad_api_key") return {"sub": rec["tenant"], "typ": "api_key", "rpm": rec["rpm"]} try: payload = jwt.decode(token, JWT_SECRET, algorithms=[ALGO]) except JWTError: raise HTTPException(status_code=401, detail="bad_jwt") return {"sub": payload["sub"], "typ": "jwt", "rpm": int(payload.get("rpm", 30))} @app.post("/v1/chat") def chat(user: dict = Depends(principal)): # Redis rate limit keyed by user["sub"] (see Redis lecture) # then call OpenAI with SERVER-side OPENAI_API_KEY return {"ok": True, "sub": user["sub"]} # 403 example: if user["typ"] != "jwt" and path.startswith("/admin"): forbid # WS/SSE: validate the same Bearer (header or ?access_token= for EventSource)

Patterns vs Anti-Patterns

Do

  • httpOnly Secure cookies or short JWTs
  • Hash product keys at rest
  • 401 missing/invalid; 403 authenticated but forbidden

Don’t

  • Vendor sk- in the SPA
  • API key in query logs forever
  • Auth only POST, forget SSE/WS

Also

  • CORS is not auth
  • Ingress TLS is not auth
  • Pydantic validation is not auth

JWT (users)

  • Fits SPAs + OIDC
  • Claims: sub, tenant, rpm
  • Rotate signing keys

API keys (machines)

  • Simple for partner backends
  • Show secret once; store hash
  • Revoke instantly in DB/Redis

Related Lectures

LectureRole
API401/403 as part of the contract
RedisRate limits per sub
Streaming / WSSame principal on upgrade/stream
DeploymentWhere JWT_SECRET and vendor keys are injected
Common Misconception

“If the frontend is behind login, the API is safe.” Anyone can replay fetch to your origin. Second: hiding the OpenAI key in a “secret” Next.js env that is actually NEXT_PUBLIC_ still ships it. Third: 401 vs 403 matter for clients. Fourth: A1111 --gradio-auth is not a multi-tenant product IdP. Fifth: long-lived JWTs in localStorage are XSS loot.

Knowledge Check

  1. Short Answer: Authn vs authz in one line. Answer: Authn = who you are; authz = what you may do/spend.
  2. True/False: OPENAI_API_KEY should be sent by the browser. Answer: False.
  3. Multiple Choice: Missing Bearer should be: (a) 401, (b) 204, (c) 301. Answer: (a).
  4. Short Answer: How should product API keys be stored? Answer: Hash (and maybe prefix) at rest; show the secret only once.
  5. True/False: CORS replaces authentication. Answer: False.
  6. Multiple Choice: Rate limits should key on: (a) authenticated principal, (b) only User-Agent, (c) CFG. Answer: (a).
  7. Short Answer: Why auth SSE/WS separately in your head? Answer: Easy to forget; EventSource/WS often cannot send headers the same way—design tokens carefully.
  8. True/False: 403 means “we know who you are but you may not.” Answer: True (typical usage).
  9. Multiple Choice: Next lecture: (a) Deployment, (b) UMAP, (c) Whisper. Answer: (a).
  10. Short Answer: Name one anti-pattern from this lecture. Answer: Any of: vendor key in SPA/localStorage, unauthenticated stream, query-string keys in logs, debug Flask auth.

Key Takeaways

  • Vendor keys ≠ user auth; never ship sk- to browsers.
  • JWT for humans, hashed API keys for machines; 401 vs 403.
  • Protect POST, SSE, WS, and job enqueue alike.
  • Quotas attach to sub via Redis.
  • Next: Deployment.
Trainer’s Guide

Lab: Add the dependency to the FastAPI chat + stream routes. Demo 401 without token, 200 with JWT, 429 after Redis rpm. Attempt to paste OPENAI_API_KEY into the frontend and fail the review.

Whiteboard: Three boxes: User JWT, Product key, Vendor key. Only the last box arrows to OpenAI.

Recap: Authenticate callers; keep vendor keys server-side. Ship it next: Deployment.