Skip to content

BearerTokenAuthMiddleware × serve(transport="both") — needs A2A sibling + serve(auth=...) shortcut #558

@bokelley

Description

@bokelley

What

BearerTokenAuthMiddleware is documented MCP-only (src/adcp/server/auth.py:62-65):

"A2A uses a different transport; wire a2a-sdk's ServerCallContext.user via a2a-sdk auth middleware on that side."

But adopters wiring a unified seller via serve(transport="both") only get the one ASGI middleware list — there's no first-class way to wire matching auth on the A2A leg through the framework.

Concrete blast: when BearerTokenAuthMiddleware is passed via asgi_middleware=[...] on serve(transport="both"), it wraps the whole binary including A2A's public /.well-known/agent-card.json discovery endpoint, so unauthenticated discovery requests get rejected with 401, breaking A2A buyer auto-discovery entirely.

Repro (production-shape, not toy)

salesagent's core/main.py: one binary serving MCP + A2A from the same handler/router, one Principal table, one token validates both transports. Buyer just picks a protocol. We hit the same gap.

Workaround we shipped (with a real security cost)

We wrote a 30-line ScopedAuthMiddleware that runs BearerTokenAuthMiddleware only on /mcp/* paths. A2A is currently unauthenticated in our build. That's a regression we accepted because the framework didn't give us a clean way to wire auth on both legs simultaneously, and we needed /.well-known/agent-card.json reachable for discovery. Filing this so we can drop the workaround.

What we'd want

Option (b) from a chat with @bokelley: add an A2ABearerAuthMiddleware sibling (validates the same token shape but populates ServerCallContext.user rather than reading an HTTP header inline) plus a serve(auth=...) shortcut that wires both legs with one config:

serve(
    handler,
    transport="both",
    auth=BearerTokenAuth(validate_token=_validate_token, header_name="x-adcp-auth"),
    ...,
)

Internally serve mounts BearerTokenAuthMiddleware on the MCP leg and A2ABearerAuthMiddleware on the A2A leg from the single auth= config. Adopters who need transport-specific auth keep the lower-level path (separate middlewares per leg).

Considered alternatives:

  • (a) Make BearerTokenAuthMiddleware transport-aware — awkward, A2A's auth model is different in shape (a2a-sdk reads ServerCallContext.user populated upstream, not an HTTP header re-validated on every request). A transport-aware mux either reimplements both auth styles or shells out to two underlying middlewares — at which point you've reinvented (b) inside a wrapper.
  • (c) Fail-fast if BearerTokenAuthMiddleware is used as outer ASGI middleware on transport="both" — punts; doesn't actually solve the problem.

Filed by

salesagent kill-nginx/M2 spike — surfaced in core/SDK_FEEDBACK.md round 3.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions