Skip to content

feat(auth): header_name + bearer_prefix_required on BearerTokenAuthMiddleware#545

Merged
bokelley merged 2 commits intomainfrom
claude/token-auth-middleware
May 4, 2026
Merged

feat(auth): header_name + bearer_prefix_required on BearerTokenAuthMiddleware#545
bokelley merged 2 commits intomainfrom
claude/token-auth-middleware

Conversation

@bokelley
Copy link
Copy Markdown
Contributor

@bokelley bokelley commented May 4, 2026

Summary

Two additive kwargs on BearerTokenAuthMiddleware to support adopters with non-standard header schemes:

  • header_name: str = "authorization" — which HTTP header carries the credential
  • bearer_prefix_required: bool = True — when False, the raw header value is passed verbatim to validate_token (no Bearer strip). Whitespace stripped to tolerate copy-pasted tokens.

Defaults preserve the spec-canonical Authorization: Bearer <token> behavior — existing adopters see zero change.

Closes the actionable part of salesagent SDK_FEEDBACK round-2 #18. The existing BearerTokenAuthMiddleware already covers most of the "TokenAuthMiddleware" ask in that doc — Principal, TokenValidator, current_principal contextvar, auth_context_factory are all here. The one gap was that salesagent's legacy server uses x-adcp-auth: <raw-token> with no Authorization header and no Bearer prefix; without these two kwargs, an adopter has to subclass the middleware and override dispatch().

Salesagent example

Before this PR, salesagent's migration would require:

class XAdcpAuthMiddleware(BearerTokenAuthMiddleware):
    async def dispatch(self, request, call_next):
        # Copy 50 LOC from the parent class, swap the header read
        ...

After this PR:

app.add_middleware(
    BearerTokenAuthMiddleware,
    validate_token=lookup_principal_token,
    header_name="x-adcp-auth",
    bearer_prefix_required=False,
)

5 LOC, no subclassing, no fork-of-dispatch maintenance.

Memory profile

Zero new request state. The two new fields are construction-time scalars on self. Same per-request ContextVar set() / reset() discipline in finally: — request-scoped, no cross-request leak. Designed with the salesagent slow-leak investigation lens.

Tests

5 new tests in test_auth_middleware.py:

  1. x-adcp-auth: <raw-token> salesagent shape — token passes through verbatim
  2. Whitespace stripping on custom-header tokens (handles copy-paste newlines)
  3. 401 when the configured custom header is absent (even if Authorization is present)
  4. bearer_prefix_required=True still enforced on custom headers (proxy-stripped scenarios)
  5. Defaults unchanged — regression guard for existing adopters

35 auth-middleware tests pass total. Full suite (tests/) passes.

Test plan

  • New tests cover happy paths + 401 paths + regression guard
  • All 35 test_auth_middleware.py tests pass
  • Mypy clean
  • No behavior change for adopters not setting the new kwargs

🤖 Generated with Claude Code

…ddleware

Adds two additive kwargs to support adopters using non-standard header
schemes — most notably salesagent's legacy ``x-adcp-auth: <raw-token>``
layout, which has no ``Authorization`` header and no ``Bearer`` prefix.

Surface:
- header_name: str = "authorization" — which HTTP header carries
  the credential. Adopters with custom-header schemes (X-Api-Key,
  x-adcp-auth, x-proxied-auth) override this.
- bearer_prefix_required: bool = True — when False, the raw header
  value is passed verbatim to validate_token (no Bearer strip).
  Whitespace is trimmed off the credential to handle copy-pasted
  tokens with trailing newlines.

Defaults preserve the spec-canonical Authorization Bearer behavior —
existing adopters not setting the new params see zero change.

Closes salesagent SDK_FEEDBACK round 2 #18 (the request was for a
TokenAuthMiddleware bridging x-adcp-auth → caller_identity; the
existing BearerTokenAuthMiddleware already covers most of this surface,
but couldn't read non-Authorization headers without subclassing).

Memory profile: zero new state. Same per-request ContextVar pattern as
before (set in dispatch, reset in finally) — request-scoped, no
cross-request leak.

Tests: 5 new tests in test_auth_middleware.py:
- x-adcp-auth raw-token scheme (salesagent shape)
- whitespace stripping on custom-header tokens
- 401 when configured header is absent
- bearer_prefix_required still enforced when True with custom header
- defaults unchanged (regression guard)

35 total auth-middleware tests pass.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
@bokelley
Copy link
Copy Markdown
Contributor Author

bokelley commented May 4, 2026

Tight, additive change — defaults preserve canonical behavior, salesagent's footgun closes with two kwargs.

Worth a docstring note (security-adjacent):

  1. bearer_prefix_required=False shifts the burden to validate_token. With the prefix gone, the middleware passes the raw header value verbatim. Adopters who flip this flag need their validate_token to be defensive about (a) non-token inputs (e.g. accidental application/json if a misconfigured client puts content-type in the wrong header), and (b) absurd lengths (a 10MB x-adcp-auth header gets handed to the validator unbounded). Both are pre-existing concerns of the validator design, but bearer_prefix_required=True was a soft pre-filter; flipping it removes that guardrail. One docstring line on the kwarg is enough.

  2. Custom header is exclusive — no fallback. The behavior is "look at header_name, ignore everything else." Worth pinning that with a test: Authorization: Bearer X AND x-adcp-auth: Y configured for header_name="x-adcp-auth" resolves Y, never X. Closes the "I expected fallback to Authorization" footgun for adopters who turn this on accidentally.

Looks fine:

  • Whitespace stripping on the raw token — right call, copy-paste tokens carry trailing newlines.
  • 5 new tests + regression guard for default behavior — coverage is right-sized for the surface change.
  • Memory-profile note is correct: construction-time scalars, no per-request state added.

Ready to merge once the docstring nits land (or punt them — they're not load-bearing).

…TokenAuthMiddleware

Addresses @bokelley's review on PR #545:
- bearer_prefix_required=False docstring now notes that the prefix
  pre-filter is removed and validate_token must be defensive about
  unexpected input shapes and unbounded lengths.
- New test confirms header_name is exclusive: when both Authorization
  and x-adcp-auth are present and header_name="x-adcp-auth", only the
  configured header reaches the validator — no silent fallback.

https://claude.ai/code/session_01T2aXRPyJmjGEQu98iia9sg
@bokelley
Copy link
Copy Markdown
Contributor Author

bokelley commented May 4, 2026

Both items addressed in the follow-up commit:

  1. Docstring note on bearer_prefix_required=False — added one line to the :param bearer_prefix_required: docstring: "setting this to False removes the prefix pre-filter; validate_token must be defensive about unexpected input shapes and unbounded lengths."

  2. Exclusive-header test — added test_custom_header_is_exclusive_no_fallback_to_authorization: sends both Authorization: Bearer tok_x and x-adcp-auth: tok_y with header_name="x-adcp-auth", asserts only tok_y reaches the validator. Closes the fallback footgun.

36/36 auth-middleware tests pass.


Generated by Claude Code

@bokelley
Copy link
Copy Markdown
Contributor Author

bokelley commented May 4, 2026

Acknowledged — both items landed, 36/36 passing. No further action from triage side.


Generated by Claude Code

@bokelley bokelley merged commit b16d18f into main May 4, 2026
15 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants