Summary
BearerTokenAuthMiddleware is named after the spec-canonical RFC 6750 Authorization: Bearer header but ships with a single configurable header_name that defaults to "authorization" and accepts no aliases. Adopters with early clients sending tokens via a custom header (x-adcp-auth, X-Api-Key, etc.) face a forced binary choice during migration: lock the middleware to the legacy header (breaks every spec-compliant new client, including @adcp/sdk comply()'s api_key probe path) OR switch to Authorization: Bearer (breaks every early adopter still on the legacy header).
The right model is: Authorization: Bearer is always accepted (the spec-canonical default), legacy aliases are an opt-in additive list. Both work simultaneously; migration is a one-way drift from legacy clients to spec-compliant ones without flag days.
Concrete adopter pain (Prebid salesagent)
The Prebid salesagent ships mcp_header_name="x-adcp-auth" because early adopters were sending tokens that way before the protocol settled on Authorization: Bearer. Every authenticated MCP call works fine for those clients. But every spec-compliant client — including the SDK's own security_baseline api_key probe, which sends Authorization: Bearer per RFC 6750 — gets 401 against the same valid token:
$ curl -i -X POST <agent>/mcp \
-H "Authorization: Bearer <TOKEN>" \
-H "Content-Type: application/json" -d '<jsonrpc body>'
HTTP/2 401
www-authenticate: Bearer
{"error":"unauthenticated"}
$ curl -i -X POST <agent>/mcp \
-H "x-adcp-auth: <TOKEN>" \
-H "Content-Type: application/json" -d '<jsonrpc body>'
HTTP/2 200
Net consequence: the salesagent fails the security_baseline/probe_api_key and assert_mechanism storyboard steps despite having RFC-correct auth — its auth path simply doesn't read the spec header. Filed locally as bokelley/salesagent#386 with a wrapper-middleware workaround that normalizes Authorization: Bearer → x-adcp-auth before delegating to BearerTokenAuthMiddleware. Every adopter mid-migration is going to write the same wrapper.
Proposed API
Replace the singular header_name: str with a list shape, with Authorization: Bearer baked in as always-accepted (cannot be turned off — it's the spec, and it's the class's own name).
class BearerTokenAuthMiddleware(BaseHTTPMiddleware):
def __init__(
self,
app: Any,
*,
validate_token: TokenValidator,
unauthenticated_response: dict[str, Any] | None = None,
# NEW: opt-in additional aliases for legacy clients. The
# spec-canonical ``Authorization: Bearer ...`` is ALWAYS
# accepted; this is purely additive.
legacy_header_aliases: list[str] | None = None,
# NEW: bearer_prefix_required applies only to the legacy
# aliases (``Authorization`` is RFC 6750 — always requires
# the ``Bearer `` prefix).
legacy_aliases_bearer_prefix_required: bool = False,
) -> None: ...
Pre-request resolution order:
Authorization: Bearer <token> (RFC 6750 §2.1) — always checked first.
- Each entry in
legacy_header_aliases in order — first non-empty wins.
- Missing / empty → 401.
Adopters with strict requirements can pin a single header by passing legacy_header_aliases=None (default) — they get spec-canonical only. Adopters migrating clients add legacy_header_aliases=["x-adcp-auth"] and both paths work simultaneously.
Backward-compat strategy
The existing header_name / bearer_prefix_required params should be:
-
Deprecated with a DeprecationWarning on construction. Map to the new shape internally — passing header_name="x-adcp-auth" produces legacy_header_aliases=["x-adcp-auth"] AND keeps Authorization: Bearer accepted (this is the behaviour change: legacy header config currently EXCLUDES the canonical header; the new shape makes them ADDITIVE).
-
Removed in the next major (6.0 / 7.0 depending on release cadence).
The behaviour change is a strict improvement for every adopter — no one wants to actively reject the spec-canonical header on a middleware named BearerTokenAuth. But it deserves a major-bump entry in the changelog because adopters who relied on header_name acting as an EXCLUSIVE filter (likely zero, but possible) will see Authorization: Bearer start arriving.
Why WWW-Authenticate: Bearer already says so
The 401 response already advertises WWW-Authenticate: Bearer (per adcp-client-python#712 once that ships, or already in _send_unauthenticated for the ASGI path). The middleware is telling buyers "send me a Bearer token" while only accepting it on a non-standard header. That's the inconsistency this issue resolves.
Migration shape for adopters
Today (broken for spec-compliant clients):
BearerTokenAuth(
validate_token=...,
mcp_header_name="x-adcp-auth",
mcp_bearer_prefix_required=False,
)
After this issue ships (works for both legacy and spec clients):
BearerTokenAuth(
validate_token=...,
legacy_header_aliases=["x-adcp-auth"],
)
Adopters who never had legacy clients (greenfield deployments) drop the kwarg entirely and inherit pure spec compliance.
Tests
The upstream test suite for BearerTokenAuthMiddleware already covers the single-header path. New tests:
Authorization: Bearer X accepted by default (no legacy_header_aliases configured)
- Legacy alias accepted when
Authorization: Bearer absent
Authorization: Bearer wins when both headers present with different tokens (or hand the divergence to a downstream audit hook — same shape as the existing dual-credential audit story).
- Empty
Authorization value falls through to legacy aliases (not auto-401).
- Deprecation warning fires on
header_name= construction.
Severity / urgency
Spec compliance. Not security-critical (the agent still rejects unauthenticated requests correctly via the legacy header path), but every adopter that exposes its endpoint to spec-compliant buyer clients sees gratuitous 401s on calls that carry valid credentials. Compliance probe surfaces it as security_baseline/probe_api_key failure.
Related
🤖 Generated with Claude Code
Summary
BearerTokenAuthMiddlewareis named after the spec-canonical RFC 6750Authorization: Bearerheader but ships with a single configurableheader_namethat defaults to"authorization"and accepts no aliases. Adopters with early clients sending tokens via a custom header (x-adcp-auth,X-Api-Key, etc.) face a forced binary choice during migration: lock the middleware to the legacy header (breaks every spec-compliant new client, including@adcp/sdkcomply()'s api_key probe path) OR switch toAuthorization: Bearer(breaks every early adopter still on the legacy header).The right model is:
Authorization: Beareris always accepted (the spec-canonical default), legacy aliases are an opt-in additive list. Both work simultaneously; migration is a one-way drift from legacy clients to spec-compliant ones without flag days.Concrete adopter pain (Prebid salesagent)
The Prebid salesagent ships
mcp_header_name="x-adcp-auth"because early adopters were sending tokens that way before the protocol settled onAuthorization: Bearer. Every authenticated MCP call works fine for those clients. But every spec-compliant client — including the SDK's ownsecurity_baselineapi_key probe, which sendsAuthorization: Bearerper RFC 6750 — gets 401 against the same valid token:Net consequence: the salesagent fails the
security_baseline/probe_api_keyandassert_mechanismstoryboard steps despite having RFC-correct auth — its auth path simply doesn't read the spec header. Filed locally asbokelley/salesagent#386with a wrapper-middleware workaround that normalizesAuthorization: Bearer→x-adcp-authbefore delegating toBearerTokenAuthMiddleware. Every adopter mid-migration is going to write the same wrapper.Proposed API
Replace the singular
header_name: strwith a list shape, withAuthorization: Bearerbaked in as always-accepted (cannot be turned off — it's the spec, and it's the class's own name).Pre-request resolution order:
Authorization: Bearer <token>(RFC 6750 §2.1) — always checked first.legacy_header_aliasesin order — first non-empty wins.Adopters with strict requirements can pin a single header by passing
legacy_header_aliases=None(default) — they get spec-canonical only. Adopters migrating clients addlegacy_header_aliases=["x-adcp-auth"]and both paths work simultaneously.Backward-compat strategy
The existing
header_name/bearer_prefix_requiredparams should be:Deprecated with a
DeprecationWarningon construction. Map to the new shape internally — passingheader_name="x-adcp-auth"produceslegacy_header_aliases=["x-adcp-auth"]AND keepsAuthorization: Beareraccepted (this is the behaviour change: legacy header config currently EXCLUDES the canonical header; the new shape makes them ADDITIVE).Removed in the next major (6.0 / 7.0 depending on release cadence).
The behaviour change is a strict improvement for every adopter — no one wants to actively reject the spec-canonical header on a middleware named
BearerTokenAuth. But it deserves a major-bump entry in the changelog because adopters who relied on header_name acting as an EXCLUSIVE filter (likely zero, but possible) will seeAuthorization: Bearerstart arriving.Why
WWW-Authenticate: Beareralready says soThe 401 response already advertises
WWW-Authenticate: Bearer(per adcp-client-python#712 once that ships, or already in_send_unauthenticatedfor the ASGI path). The middleware is telling buyers "send me a Bearer token" while only accepting it on a non-standard header. That's the inconsistency this issue resolves.Migration shape for adopters
Today (broken for spec-compliant clients):
After this issue ships (works for both legacy and spec clients):
Adopters who never had legacy clients (greenfield deployments) drop the kwarg entirely and inherit pure spec compliance.
Tests
The upstream test suite for
BearerTokenAuthMiddlewarealready covers the single-header path. New tests:Authorization: Bearer Xaccepted by default (no legacy_header_aliases configured)Authorization: BearerabsentAuthorization: Bearerwins when both headers present with different tokens (or hand the divergence to a downstream audit hook — same shape as the existing dual-credential audit story).Authorizationvalue falls through to legacy aliases (not auto-401).header_name=construction.Severity / urgency
Spec compliance. Not security-critical (the agent still rejects unauthenticated requests correctly via the legacy header path), but every adopter that exposes its endpoint to spec-compliant buyer clients sees gratuitous 401s on calls that carry valid credentials. Compliance probe surfaces it as
security_baseline/probe_api_keyfailure.Related
BearerTokenAuthMiddlewaredefect (WWW-Authenticateheader omitted on 401). Same module, same auth path.IdempotencyStore.wrap.Authorization: Bearer→x-adcp-auth). Will be deleted when this issue ships.🤖 Generated with Claude Code