Skip to content

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

1
2
3
4
5
6
7
8
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-path β†’ request.state.token
Auth on the client side pass the token via client.<operationId>(headers={"Authorization": ...})