docs: add OpenAPI-first wiki (overview, components, use cases for templates/client/codegen, design, security, error handling, testing) and refresh lib docs index
This commit is contained in:
144
docs/wiki/05_security.md
Normal file
144
docs/wiki/05_security.md
Normal file
@@ -0,0 +1,144 @@
|
||||
# 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)
|
||||
Reference in New Issue
Block a user