A correct failure map makes blame assignment concrete. The model produced text, but the backend decided whether POST /v1/tools/refund.create executed, and the backend is where we can prove the principal’s scope, require approval for irreversible actions, and attach an idempotency key so retries do not double spend. Once we enforce that boundary, prompt injection attempts become inputs that fail validation and authorization, not instructions that become money movement.
We assume a working grasp of HTTP status codes, meaning we already know what a refusal response represents on the wire.
We assume JWT basics, meaning we can verify a signature and check issuer, audience, and expiry using a vetted library.
We assume FastAPI dependency injection, meaning we can attach authentication, authorization, and quota checks as request scoped dependencies instead of scattering checks through handlers.
The artifact we build across the course is a reference backend pipeline where each stage has a clear refusal point, each log record has stable fields reviewers can reason about, and each regression case ties to one control. The goal is not a clever prompt. The goal is a backend that can safely do RAG over account history, call refund.create only when u_123 is entitled and approved, and run analysis scripts without granting network or secret access. The diagram that follows fixes the request path we will harden stage by stage.
Generate custom courses on any topic — with hands-on practice, AI guidance, and visuals built in.
Already have an account?
A FastAPI support copilot backend usually fails in an ordinary way. The backend reads account history for acct_987, a model suggests next steps, and a tool call issues a refund for txn_456 because the support ticket text sounded confident. Nothing exotic happened. The application accepted untrusted text from the ticket, treated the suggestion as authorization, and executed refund.create with the service’s privileges instead of enforcing the end user’s entitlements and an approval gate.
The security work in this course is building the missing enforcement stages in the only places that can enforce them. Prompt wording cannot stop a tool router from sending a request, and a model cannot prove that u_123 is allowed to refund txn_456. The backend either refuses the request before the tool runs, or the backend runs the tool and spends real money.
We keep one concrete anchor for every lesson so each control lands on the same moving parts. The tenant is t_acme, the end user is u_123, the account is acct_987, and the transaction candidate for a refund is txn_456. The only refund tool is named refund.create, and the only route that reaches it is POST /v1/tools/refund.create.
We also fix the only scope name that ever permits a refund. A principal that lacks support:refund may still chat about policies, summarize account history, and draft a response, but the principal must not execute refund.create. Model choice stays explicit but abstract. The application calls a chat model through $CHAT_MODEL and uses $MODERATION_MODEL where a later lesson needs a dedicated screening call.
Once the names are fixed, we can map a single request path and mark where untrusted data enters. The caller message is untrusted even when it came from an authenticated support agent, because the message can carry copied customer text or ticket content. Retrieved account history chunks are untrusted because retrieval returns whatever the store contains, including stale notes and free text that can try to steer the model. Tool results are untrusted because downstream systems can return unexpected strings or fields, and model output is untrusted because the model optimizes for plausible text, not for policy compliance.
The course builds only the enforcement points that can turn those untrusted inputs into a bounded, auditable action. We authenticate and bind a principal so every request carries t_acme and u_123 as server verified facts, not as caller supplied headers. We meter quota before the provider call so one endpoint cannot spend a month of tokens overnight. We authorize retrieval at the call site so the backend only retrieves chunks that u_123 is entitled to see, and we treat every retrieved chunk as untrusted content anyway. We validate structured model output before any router, parser, template, or tool sees it. We authorize and approve tool calls so refund.create cannot run just because the model emitted a persuasive suggestion. We sandbox analysis scripts so generated code cannot reach the network or secrets. We audit every refusal and every executed action with enough fields to explain who did what, for which tenant, and why the backend allowed it.
When the support ticket text tries to trigger a refund, which stage could have refused the refund before any tool ran?