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

4.5 KiB
Raw Permalink Blame History

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) 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

# 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-pathrequest.state.token
Auth on the client side pass the token via client.<operationId>(headers={"Authorization": ...})