4.5 KiB
Security — Auth Driven by the Spec, Not by Hand
The most distinctive thing about openapi-first security is that you never write middleware or Depends(authenticate) calls yourself. All authentication is declared in the OpenAPI document and enforced automatically.
🗺️ 1. Where Security Lives
Two places in the spec, both respected:
1.1 components.securitySchemes — the inventory
components:
securitySchemes:
internalBearer:
type: http
scheme: bearer
bearerFormat: JWT
x-server-url: "{AUTH_SERVER_URL}"
x-introspect-path: "/introspect"
x-server-url and x-introspect-path are library extensions that point at a JWT introspection endpoint (see Introspection).
1.2 security — per-operation (or global) requirements
security:
- internalBearer: [] # applied to every operation by default
paths:
/pets:
get:
operationId: list_pets
# inherits: security: [{internalBearer: []}]
post:
operationId: create_pet
security: [] # override — public endpoint
OpenAPI dynamic-scoping rules apply: an operation-level security replaces the global list (it does not merge).
📖 2. Reading the Spec
security.py exposes two functions:
| Function | Returns | Purpose |
|---|---|---|
parse_security_schemes(spec) |
dict[str, dict] |
Collect schemes, resolve {ENV_VAR} placeholders |
make_security_dependencies(spec, schemes) |
dict[str, list[Depends]] (keyed METHOD:/path) |
Effective per-operation security deps |
Env placeholders of the form {NAME} are resolved once at startup from os.environ. This is how you avoid embedding credentials or auth-service URLs in the committed spec.
🔐 3. The Bearer Dependency
make_security_dependencies builds a FastAPI dependency for type: http, scheme: bearer.
Two modes, decided by whether an introspection endpoint is configured:
3.1 With introspection (x-introspect-path)
- Reads
Authorization: Bearer <token> - POSTs
{"token": "<token>"}to{x-server-url}{x-introspect-path}synchronously via the bundled httpx client - Expects a response with
active: true - On valid:
{"user": ...}from the introspection body →request.state.user - On failure:
401(missing/invalid token) or503(auth service unreachable)
3.2 Without introspection
- Validates only that a Bearer token is present
- Stores it on
request.state.token; no remote call
🔗 4. Wiring It Together
# server-side
from openapi_first.loader import load_openapi
from openapi_first.security import (
parse_security_schemes,
make_security_dependencies,
)
from openapi_first.app import OpenAPIFirstApp
import routes
spec = load_openapi("openapi.yaml")
schemes = parse_security_schemes(spec)
security_deps = make_security_dependencies(spec, schemes)
app = OpenAPIFirstApp(
openapi_path="openapi.yaml",
routes_module=routes,
)
OpenAPIFirstApp already does this internally — the snippet above shows what it encapsulates (and what you use directly if you assemble the pieces by hand).
🧪 5. Testing Security
Because handlers are plain callables, security is the one place FastAPI's TestClient earns its keep:
from fastapi.testclient import TestClient
def test_unauthenticated_is_401(app, overrides):
with TestClient(app) as client:
r = client.get("/pets")
assert r.status_code == 401
def test_invalid_token_using_fake_introspector(app):
# Point x-introspect-path at a stub uvicorn/TestServer returning active:false
with TestClient(app) as client:
r = client.get("/pets", headers={"Authorization": "Bearer nope"})
assert r.status_code == 401
See Testing for the full recipe, including how tests stub the introspection server.
🛡️ 7. Common Patterns
| Pattern | How |
|---|---|
| Public endpoint | security: [] on the operation |
| Whole-spec auth | top-level security: (applies to all) |
| Route-specific scheme | replace security on that operation |
| Env-driven auth URL | x-server-url: "{AUTH_SERVER_URL}" |
| Offline token carry | scheme without x-introspect-path → request.state.token |
| Auth on the client side | pass the token via client.<operationId>(headers={"Authorization": ...}) |