Files
wehub-resource-sync 0ef5fcb1c5
Security / Dependency audit (pip-audit) (push) Has been cancelled
Security / CodeQL (javascript-typescript) (push) Has been cancelled
Security / CodeQL (python) (push) Has been cancelled
Security / Secret scan (gitleaks) (push) Has been cancelled
rust / test (ubuntu) (push) Has been cancelled
rust / simulator e2e (macos-latest) (push) Has been cancelled
rust / simulator e2e (ubuntu-latest) (push) Has been cancelled
rust / simulator e2e (windows-latest) (push) Has been cancelled
rust / wheels (aarch64-apple-darwin) (push) Has been cancelled
rust / wheels (x86_64-unknown-linux-gnu) (push) Has been cancelled
rust / wheels (x86_64-apple-darwin) (push) Has been cancelled
rust / audit (push) Has been cancelled
rust / parity (nightly, allowed to fail during Phase 0) (push) Has been cancelled
CI / commitlint (push) Has been skipped
Dev Containers / validate (.devcontainer/devcontainer.json, default) (push) Failing after 0s
Dev Containers / validate (.devcontainer/memory-stack/devcontainer.json, memory-stack) (push) Failing after 0s
Dev Containers / validate-worktree (push) Failing after 0s
CI / changes (push) Failing after 4s
Deploy Documentation / validate (push) Has been skipped
Deploy Documentation / deploy (push) Failing after 1s
Init Native E2E / init-native (ubuntu-latest, claude) (push) Failing after 1s
Init Native E2E / init-native (ubuntu-latest, codex) (push) Failing after 1s
Install Native E2E / install-native (ubuntu-latest) (push) Failing after 1s
OpenCode Plugin / typecheck + build + test (push) Failing after 1s
Init Native E2E / init-native (ubuntu-latest, copilot) (push) Failing after 1s
Release Please / release-please (push) Failing after 1s
Wrap E2E / docker-wrap-e2e (push) Failing after 1s
Wrap Native E2E / wrap-native (ubuntu-latest) (push) Failing after 1s
Init E2E / docker-init-e2e (push) Failing after 4s
Merge Conflicts / merge-conflicts (push) Failing after 4s
CI / lint (push) Has been cancelled
CI / build-wheel (push) Has been cancelled
CI / build-wheel-windows (push) Has been cancelled
CI / prefetch-model (push) Has been cancelled
CI / test-dashboard-ui (push) Has been cancelled
CI / test (1) (push) Has been cancelled
CI / test (2) (push) Has been cancelled
CI / test (3) (push) Has been cancelled
CI / test (4) (push) Has been cancelled
CI / test-extras (push) Has been cancelled
CI / test-agno (push) Has been cancelled
CI / build (push) Has been cancelled
CI / workflow-validation (push) Has been cancelled
CI / docker-native-e2e (push) Has been cancelled
CI / windows-native-wrapper (push) Has been cancelled
CI / macos-native-wrapper (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-code-nonroot name:code-nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-code-slim name:code-slim]) (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-code-slim-nonroot name:code-slim-nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-nonroot name:nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-slim name:slim]) (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-slim-nonroot name:slim-nonroot]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime name:]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-code name:code]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-code-nonroot name:code-nonroot]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-code-slim name:code-slim]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-code-slim-nonroot name:code-slim-nonroot]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-nonroot name:nonroot]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-slim name:slim]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-slim-nonroot name:slim-nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime name:]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-code name:code]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-code-nonroot name:code-nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-code-slim name:code-slim]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-code-slim-nonroot name:code-slim-nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-nonroot name:nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-slim name:slim]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-slim-nonroot name:slim-nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime name:]) (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-code name:code]) (push) Has been cancelled
Docker / promote-latest (push) Has been cancelled
Init Native E2E / init-native (macos-latest, claude) (push) Has been cancelled
Init Native E2E / init-native (macos-latest, codex) (push) Has been cancelled
Init Native E2E / init-native (macos-latest, copilot) (push) Has been cancelled
Install Native E2E / install-native (macos-latest) (push) Has been cancelled
Wrap Native E2E / wrap-native (macos-latest) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:03:20 +08:00

6.4 KiB

Spec — headroom-oauth2 (generic OAuth2 client-credentials upstream auth)

Summary

A vendor-neutral proxy extension (registers on Headroom's headroom.proxy_extension seam) that mints an OAuth2 client-credentials (RFC 6749 §4.4) bearer from a configured token endpoint and injects it as the upstream Authorization on every proxied request. Lets Headroom front any gateway that requires a minted machine token (not a static API key) — with zero core changes and no vendor specifics (the gateway is entirely config/env).

It complements #510 (env-var auth), which assumes a long-lived static key; this covers the "mint-then-refresh a short-lived token" case.

API surface (config / CLI / env)

Opt-in only, via Headroom's existing flags — no new CLI flags:

headroom proxy --backend litellm-openai --proxy-extension oauth2
# or HEADROOM_PROXY_EXTENSIONS=oauth2

All configuration is env (12-factor; nothing baked in):

Env var Required Meaning
HEADROOM_OAUTH2_TOKEN_URL yes (else no-op) OAuth2 token endpoint; must be https (loopback http allowed for tests)
HEADROOM_OAUTH2_CLIENT_ID / _CLIENT_SECRET yes client credentials
HEADROOM_OAUTH2_SCOPES no space/comma-separated scopes
HEADROOM_OAUTH2_AUDIENCE no audience form param
HEADROOM_OAUTH2_RESOURCE no RFC 8707 target resource form param
HEADROOM_OAUTH2_GRANT_TYPE no default client_credentials
HEADROOM_OAUTH2_AUTH_STYLE no post (form creds) or basic (HTTP Basic)
HEADROOM_OAUTH2_HEADERS no static upstream headers, K=V,K2=V2 (control chars rejected)
HEADROOM_OAUTH2_TIMEOUT / _SKEW no token request timeout / pre-expiry refresh skew (s)
HEADROOM_OAUTH2_ALLOW_INSECURE no 1 to allow a non-loopback http token_url (discouraged)

Public Python API: OAuth2ClientCredentials, OAuth2Middleware, OAuth2Error, install, provider_from_env, parse_headers.

Changes to existing behavior / defaults / compatibility

  • None unless explicitly enabled. The entry point is dormant until --proxy-extension oauth2 is passed, and even then a no-op unless HEADROOM_OAUTH2_TOKEN_URL is set.
  • When active, it overwrites the request Authorization header with the minted bearer before the backend runs. The client's own Authorization/x-api-key is intentionally replaced (the proxy authenticates to the gateway on the client's behalf). Compatibility note: because the request then carries a bearer, Headroom classifies it as OAuth-mode auth — same as supplying a bearer yourself; no new classification path.
  • No change to defaults, the request/response body, model routing, or compression.

User stories (Given / When / Then)

  • Golden pathGiven a proxy started with --proxy-extension oauth2 and valid TOKEN_URL/CLIENT_ID/CLIENT_SECRET, When a client sends /v1/messages, Then the extension mints (or reuses a cached) bearer and the upstream receives Authorization: Bearer <minted> plus any static headers; the client never sees the secret.
  • Edge: token endpoint downGiven an unreachable/erroring TOKEN_URL, When a request arrives, Then the proxy returns 502 upstream_auth_error (no upstream call, no secret/body leak) and stays up; the next request retries.
  • Edge: wrong backendGiven --backend bedrock (env-auth), When the extension installs, Then it logs a loud warning that the injected bearer will have no effect and to use an OpenAI-compatible/passthrough backend.

Failure modes & recovery

Failure Behavior
Missing/invalid config at startup install() raises -> proxy fails closed (won't start mis-auth'd)
Token endpoint unreachable / non-2xx / non-JSON / no access_token OAuth2Error -> 502, per-request, proxy stays up, retried next request
expires_in = 0/negative/absent clamped to a positive TTL (never stale, never per-request mint)
Concurrent first requests single-flight lock -> exactly one mint per refresh
Malformed HEADROOM_OAUTH2_HEADERS (CR/LF) offending pair dropped with a warning (no header injection)

Resilience (Docker / native / wrappers / providers / multi-process)

  • Native & Docker: identical; pure env-driven, std-lib only. Token minted via urllib against the system cert store, so a corporate-injected CA is trusted with no bundled roots (works in SSL-inspection networks).
  • Wrappers (headroom wrap, agent hooks): the extension lives at the proxy layer, so anything routed through the proxy inherits it transparently.
  • Providers: effective for OpenAI-compatible / passthrough litellm backends (those that forward the request bearer upstream). bedrock/vertex/sagemaker authenticate from env and ignore the bearer -> the extension warns and is a no-op there.
  • Multi-process (multiple workers): the token cache is per-process; each worker mints/refreshes independently. Acceptable for client-credentials (idempotent, low rate); no shared state, no cross-process lock needed. Documented so operators can size token-endpoint rate limits.

Security & privacy

  • Secrets are env-only; never logged and never returned to the client.
  • The IdP error body is drained, not surfaced (may echo sensitive context).
  • token_url is https-enforced (loopback exception for tests; explicit opt-out env).
  • The minted bearer is sent only to the configured upstream; the client's inbound credential is replaced, not forwarded onward.

Observability / logging / telemetry

  • INFO on install (token_url + auth style, no secrets) and on each mint (ttl, scopes).
  • WARNING on mint failure, env-auth-backend no-op, and dropped malformed static headers.
  • No metrics/telemetry emitted; piggybacks on Headroom's existing request logging. (A future counter for mint/refresh/failure could be added if maintainers want it.)

Rollback / migration

  • No migration — additive and opt-in; existing deployments are unaffected.
  • Instant rollback: drop --proxy-extension oauth2 (or unset HEADROOM_PROXY_EXTENSIONS), or uninstall the package. No state to clean up, no config format changes.

Dependencies

  • Runtime: standard library only (no new core dependency). litellm is touched only if HEADROOM_OAUTH2_HEADERS is set, and it is already a Headroom backend dependency — declared here as the optional [litellm] extra, not a hard requirement.