Commit graph

2 commits

Author SHA1 Message Date
Abhinav Kumar Singh
c85abf7a87
fix(oauth2): make repository lint checks pass
## Description

Fixes #2895

The repository-wide Ruff command failed on the bundled OAuth2 plugin.
This change sorts the public export list, narrows the optional LiteLLM
setup exception handling to expected failures, and replaces the silent
HTTP error-body drain with explicit handling and debug logging.

## Type of Change

- [x] Bug fix (non-breaking change that fixes an issue)
- [ ] New feature (non-breaking change that adds functionality)
- [ ] Breaking change
- [ ] Documentation update
- [ ] Performance improvement
- [ ] Code refactoring (no functional changes)

## Changes Made

- Sorted headroom_oauth2.__all__ according to Ruff RUF022.
- Replaced the blind install-time Exception catch with explicit
ImportError, AttributeError, OSError, TypeError, and ValueError
handling.
- Replaced the silent HTTPError body-drain pass with explicit
HTTPException, OSError, and ValueError handling plus debug logging.
- Added regression coverage for body-drain failures and invalid LiteLLM
header state.

## Testing

- [x] Unit tests pass
- [x] Linting passes (ruff check .)
- [x] Type checking passes (mypy headroom)
- [x] New tests added
- [x] Manual testing performed

### Test Output

    ruff 0.15.17
    ruff check .
    All checks passed!

    ruff format --check .
    1382 files already formatted

    python -m mypy headroom
    Success: no issues found in 515 source files

PYTHONPATH=plugins/headroom-oauth2/src python -m pytest -q
plugins/headroom-oauth2/tests
    39 passed in 11.12s

Full Python pytest was attempted: 8,878 tests were collected, but
collection stopped with 174 environment errors because the required
compiled headroom._core extension is unavailable in this Windows
checkout. 18 tests were skipped.

## Real Behavior Proof

- Environment: Windows PowerShell, Python 3.12, Ruff 0.15.17.
- Exact command / steps: Ran the OAuth2 test suite with PYTHONPATH
pointing to plugins/headroom-oauth2/src. Its local HTTPServer fixture
exercised real urllib token minting, cached refresh, HTTP error
handling, and middleware injection.
- Observed result: 39 tests passed, including real loopback token
minting and the new failure-path tests; repository-wide Ruff completed
with no diagnostics.
- Not tested: External identity-provider traffic and the full Python
suite after native extension build, because the local Windows toolchain
cannot build headroom._core.

## Review Readiness

- [x] I have performed a self-review
- [x] This PR is ready for human review

## Checklist

- [x] My code follows the project's style guidelines
- [x] I have performed a self-review of the code
- [x] I have commented my code where needed
- [ ] I have made corresponding changes to the documentation (not
needed; behavior and lint handling are covered by existing
comments/tests)
- [x] My changes generate no new warnings
- [x] I have added tests that prove the fix is effective
- [ ] New and existing full-repository unit tests pass locally (blocked
by missing native headroom._core)
- [x] I did not edit CHANGELOG.md

## Additional Notes

No dependencies or public API behavior changed. Expected environment and
transport failures remain handled; unexpected programmer errors now
propagate instead of being silently swallowed. The OAuth2 plugin remains
standard-library-only.

---------

Co-authored-by: Tejas Chopra <tejas@Tejass-MacBook-Pro.local>
2026-08-11 14:25:25 -07:00
Khalid Shaikh
eb2e50feb2
feat: add OAuth2 client-credentials upstream-auth proxy extension (#778) (#784)
## 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.
2026-06-11 11:42:25 -05:00