# 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 ```yaml 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](#-5-introspection)). ### 1.2 `security` β€” per-operation (or global) requirements ```yaml 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 ` - POSTs `{"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) or `503` (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 ```python # 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: ```python 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](07_testing.md) 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.(headers={"Authorization": ...})` | --- ## Related - [02 – Components](02_components.md) Β· [04 – Design](04_design.md) Β· [06 – Error Handling](06_error_handling.md) Β· [07 – Testing](07_testing.md)