## What & why
Adds **`headroom-oauth2`** under `plugins/` — a generic, vendor-neutral
proxy extension that mints an OAuth2 **client-credentials** (RFC 6749
§4.4) bearer from a configured token endpoint and injects it as the
upstream `Authorization` on each proxied request, via the opt-in
`headroom.proxy_extension` seam. **No core changes.**
This lets headroom front any gateway that requires a *minted,
short-lived machine token* rather than a static API key. It complements
**#510** (env-var/static-key auth) rather than replacing it.
Implements **#778** (feature request). Opening the implementation
alongside the issue so there's something concrete to react to — **happy
to hold/rework pending a 👍 from a maintainer**, per CONTRIBUTING.
## Spec
Full spec in
[`plugins/headroom-oauth2/SPEC.md`](plugins/headroom-oauth2/SPEC.md)
(API surface, behavior/compat, user stories, failure modes, resilience
incl. multi-process, security, observability, rollback). Highlights:
- **Opt-in & no-op by default:** dormant until `--proxy-extension
oauth2`, and a no-op unless `HEADROOM_OAUTH2_TOKEN_URL` is set. No
change to defaults, body, routing, or compression.
- **Config is 100% env** (no new CLI flags):
token_url/client_id/secret/scopes/audience, RFC 8707 `resource`,
`post`|`basic` auth style, static upstream headers, timeout/skew.
- **Token caching + single-flight refresh**; `expires_in` clamped to a
positive TTL.
- **Fails closed** on misconfig; returns `502 upstream_auth_error` on
mint failure **without leaking the IdP error body**; `token_url` is
**https-enforced** (loopback exempt for tests).
- **Standard-library only** (token minted via `urllib` → system cert
store, so it works behind corporate SSL inspection). `litellm` is
touched only for static headers and is an optional extra, not a core
dep.
- **Effective for** OpenAI-compatible / passthrough litellm backends.
`bedrock`/`vertex`/`sagemaker` auth from env and ignore a forwarded
bearer → the extension **warns loudly** and is a no-op there.
## Tests
37 tests covering behavior **and** failure modes (`ruff check`/`format`
clean, **98% coverage**): post/basic mint, caching, single-flight (cold
+ on-refresh, exact mint counts under concurrency), https enforcement +
`localhost` rejection + `::1`, `expires_in`
clamp/float/missing/non-numeric, `extra_params` cannot clobber canonical
fields, bad-status/non-JSON/no-token/unreachable (asserting no
secret/body leak), ASGI middleware (inject, non-http passthrough, 502 +
`no-store`, missing `headers` key), and `install()`
(no-op/fail-closed/bad-timeout/env-auth-backend-warning/static-headers).
## Real behavior proof
- **Setup:** Linux aarch64, Python 3.13.5, `headroom-ai` 0.23.0, real
`headroom proxy` process.
- **Steps:** started `headroom proxy --backend litellm-openai
--proxy-extension oauth2` with `HEADROOM_OAUTH2_*` env pointed at a
local OAuth2 token endpoint; an upstream echo server captured what the
backend received; sent two `/v1/messages` requests through the proxy.
- **Observed (copied output):**
```
PROXY: headroom-oauth2: client-credentials auth installed
(token_url=…/token, style=post)
MINTS (across 2 requests): 1 # token cached + reused -> 1 mint for 2
requests
UPSTREAM RECEIVED: auth=Bearer MINTED-FROM-IDP-…
static=generic-static-header
SECRET LEAK CHECK (client_secret in proxy logs): 0
```
→ The minted bearer (not the placeholder backend key) and the configured
static header reached the upstream; the client's inbound credential was
replaced; the client secret never appeared in logs; caching worked.
- **What I did *not* test:** a live commercial IdP
(Entra/Okta/Auth0/etc.) and a live cloud gateway — the token endpoint
and upstream here are local stand-ins. Also not tested: multi-worker
(gunicorn) deployment, and Python 3.10/3.11 (developed on 3.13).
## Placement
Proposed as a standalone installable package under
`plugins/headroom-oauth2/` (registers via the entry-point seam; `pip
install -e plugins/headroom-oauth2`). Open to baking it into core or
publishing it separately — maintainer's call.
2.1 KiB
headroom-oauth2
Generic OAuth2 client-credentials upstream-auth extension for the Headroom proxy.
When Headroom routes to an OpenAI-compatible backend that is protected by an
OAuth2 client-credentials flow (enterprise AI gateways, Azure AD / Entra, Okta,
Auth0, Keycloak, Cognito, …), this extension mints a bearer token from a
configurable token endpoint, caches + refreshes it (single-flight), and injects
Authorization: Bearer <token> on each upstream request. Optional static upstream
headers are sent via litellm. Fully vendor-neutral — no provider is hard-coded.
It plugs into Headroom's public headroom.proxy_extension entry-point seam, so it
is fully out-of-tree and opt-in.
Install & enable
pip install headroom-oauth2
headroom proxy --backend litellm-openai --proxy-extension oauth2
Configure (env; no-op unless HEADROOM_OAUTH2_TOKEN_URL is set)
| Env | Meaning |
|---|---|
HEADROOM_OAUTH2_TOKEN_URL |
token endpoint (client_credentials grant) |
HEADROOM_OAUTH2_CLIENT_ID / _CLIENT_SECRET |
credentials (secrets) |
HEADROOM_OAUTH2_SCOPES |
space/comma-separated scopes |
HEADROOM_OAUTH2_AUDIENCE |
optional audience |
HEADROOM_OAUTH2_GRANT_TYPE |
default client_credentials |
HEADROOM_OAUTH2_AUTH_STYLE |
post (form creds) or basic (HTTP Basic) |
HEADROOM_OAUTH2_HEADERS |
static upstream headers, K=V,K2=V2 |
Tokens are minted with the standard library (urllib, system cert store), which
works behind corporate SSL-inspection where bundled-root TLS stacks fail.
Effective backends: the injected bearer reaches the upstream only for OpenAI-compatible /
passthrough litellm providers. bedrock / vertex / sagemaker authenticate from env and
ignore it, so this extension is a no-op there (it logs a warning at startup).
Transport: token_url must be https (loopback http is allowed for tests; set
HEADROOM_OAUTH2_ALLOW_INSECURE=1 to override). Tokens are minted with the standard library
(urllib, system cert store), so a corporate-injected CA is trusted without bundling roots.