AI endpoints are sensitive endpoints. Put auth on them.
Look back over this series and you’ll notice a word doing a lot of quiet work: per user. Per-user caps and quotas. Per-user token limits. Per-user attribution. Every one of those controls assumes you can answer a basic question — who is calling? — and that answer is authentication. It’s the base the rest of the security module stands on.
The endpoint teams under-protect
An LLM route is, functionally, one of your most sensitive endpoints: it spends money on every call and it can be abused into someone’s free ChatGPT or a liability. And yet it routinely ships with less authentication than a plain CRUD endpoint — sometimes none at all, just an open path the frontend posts to.
Every control here assumes you know who's calling — that's auth.
The reframe: treat your AI endpoint like a payment endpoint. You wouldn’t expose “charge a card” anonymously to the internet. “Spend tokens” is the same category of action.
What “auth on AI endpoints” means
- Authenticate the caller. A real identity — a logged-in session, a JWT, or an API key you issue — never anonymous, uncontrolled traffic. Even a public product should hand out a token you can attribute and revoke, not a naked open route.
- Authorize per identity. That identity resolves to their policy: quota, daily budget, allowed models, rate limit. This is the hinge the whole series turns on — caps and token limits are meaningless without an identity to attach them to.
- Attribute every call. The
run_idand owner tag from the per-run breaker ride on the authenticated identity — so cost reporting, enforcement, and the kill switch all share one key. - Don’t hand out the provider key as your “auth.” Issue your own per-user virtual keys (Day 11); the real provider credential stays server-side, and you can revoke or re-scope one user without rotating the master key.
The common patterns
- Server-side app. The user is already authenticated to your app; the LLM call happens server-side under that identity, so the gateway sees a real user id and applies their policy. Nothing sensitive reaches the client.
- Public API product. Issue an API key per customer, each mapped to a gateway virtual key with its own budget, rate limit, and model allow-list. Revoke a key and that customer is off, instantly.
- Never: the raw provider key in the client (Day 11), or an unauthenticated public endpoint (Day 9).
None of this is novel security — it’s identity, sessions, per-user authorization, and revocation, the things you already do across your app. The only shift is recognizing that the LLM route is an unusually expensive and abusable resource, and deserves at least the auth you give your billing code.
The gate, in code
Concretely, it’s one dependency wrapped around the AI route — it authenticates the caller and hands back who they are before the handler body runs. A FastAPI version:
from fastapi import Depends, FastAPI, HTTPException, Security
from fastapi.security import APIKeyHeader
app = FastAPI()
api_key_header = APIKeyHeader(name="X-API-Key") # where the caller's key rides
def require_key(key: str = Security(api_key_header)) -> Caller:
caller = lookup_key(key) # your own issued keys, hashed in the DB
if caller is None or caller.revoked: # unknown or revoked → no model call
raise HTTPException(status_code=401, detail="invalid or revoked key")
return caller # a real identity: id, quota, allowed models
@app.post("/v1/chat")
def chat(body: ChatRequest, caller: Caller = Depends(require_key)):
# auth ran FIRST — nothing below executes for anonymous traffic
enforce_quota(caller) # per-identity cap/breaker keys off caller.id
return run_llm(body, owner=caller.id) # attribution rides the authenticated id
FastAPI resolves Depends(require_key) before it ever enters the handler, so an unauthenticated request
401s at the door and never reaches the model call. The dependency doesn’t just say yes or no — it
returns the identity, and that same caller.id is what the quota check and the
per-run breaker key off, and what tags the run for attribution.
Swap the header lookup for JWT verification and the shape is identical: authenticate, resolve an identity,
attach it, then spend tokens.
One honest boundary, to keep the layers straight: auth stops anonymous abuse and unlocks every per-user control. It does not stop a legitimate account that’s been compromised (that’s quotas plus anomaly detection) or prompt injection (that’s Days 7–8). Auth is the foundation, not the whole house.
Next in the series — Day 15: a fail-closed AI gateway you can copy — where every control from the last two weeks composes into one door. (Day 14’s kill switch already got its own post, so we go straight to assembly.)
If your AI endpoint is reachable without a real, revocable identity behind it, none of the per-user controls you think you have are actually enforceable — and wiring that up is part of what I do. Every message comes straight to me — I read and reply to each one myself, usually within a day, and what readers send shapes what I build next. It’s just me for now, so that’s genuinely true; it won’t be forever. Send me your setup and I’ll tell you what’s calling your model unauthenticated — free, within a business day.