Files
openapi-first/docs/wiki/05_security.md

145 lines
4.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 <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) 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.<operationId>(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)