Skip to content

fix(server): propagate auth ContextVars through A2A dispatch boundary#591

Draft
bokelley wants to merge 1 commit intomainfrom
claude/issue-590-a2a-contextvar-propagation
Draft

fix(server): propagate auth ContextVars through A2A dispatch boundary#591
bokelley wants to merge 1 commit intomainfrom
claude/issue-590-a2a-contextvar-propagation

Conversation

@bokelley
Copy link
Copy Markdown
Contributor

@bokelley bokelley commented May 5, 2026

Closes #590

Summary

A2ABearerAuthMiddleware validated bearer tokens on the A2A transport but only stored the authenticated Principal in scope["user"] and scope["auth"]. It never set the module-level ContextVars (current_principal, current_tenant, current_principal_metadata) that BearerTokenAuthMiddleware sets on the MCP path. As a result, current_principal.get() returned None inside every A2A handler call even after successful auth — breaking the documented contract ("On success, populates current_principal, current_tenant, and current_principal_metadata for the duration of the downstream call") for the A2A leg.

Root effect: every adopter whose platform method reads current_principal.get() directly, or whose require_auth tenant policy depends on auth_context_factory populating AuthInfo, was locked out of A2A while MCP worked fine.

The fix mirrors the existing BearerTokenAuthMiddleware try/finally pattern:

  • On auth success: set all three ContextVars from the validated Principal, reset unconditionally in finally.
  • On OPTIONS/discovery pass-throughs: set all three to None (clearing any stale value from an enclosing task context), reset in finally. Matches the MCP middleware's discovery branch.
  • On non-HTTP scopes (lifespan, websocket): pass through unchanged — skill handlers don't run on these paths.

What was tested

  • pytest tests/test_serve_auth_both.py30 passed (including 2 new regression tests)
  • pytest tests/test_auth_middleware.py tests/test_a2a_server.py tests/test_decisioning_dispatch.py123 passed
  • Full non-integration suite: 4120 passed, 0 failed (1 pre-existing network flake in test_ip_pinned_transport.py::test_real_tls_handshake_still_validates_hostname excluded — hits example.com, unrelated)
  • ruff check src/adcp/server/auth.py — clean
  • mypy src/adcp/server/auth.py --ignore-missing-imports — no new errors (pre-existing BaseHTTPMiddleware stub issue at line 197 unchanged)

Pre-PR review:

  • code-reviewer: approved — no blockers; nits: current_principal_metadata not asserted in unit test (coverage gap, not a correctness risk — set/reset logic is structurally identical for all three tokens); OPTIONS/discovery reset-in-finally not explicitly tested (would require inner app to raise, and ContextVar resets to None which is also the default)
  • security-reviewer: approved — 401 path (inside try before any token set) confirmed safe: finally is a no-op when all tokens are None; clearing to None on OPTIONS/discovery confirmed correct and matching the MCP middleware's discovery branch; no new attack surface

Triage-managed PR. This bot does not currently iterate on
review comments or PR conversation threads (only on the source
issue). To unblock:

  • Push fixup commits directly: gh pr checkout <num>
    fix → push.
  • Or re-trigger: comment /triage execute on the source
    issue.

See adcp#3121
for context.

Session: https://claude.ai/code/session_01ETystgT4HH4ZHaAziszjBF


Generated by Claude Code

A2ABearerAuthMiddleware validated bearer tokens but only wrote the
principal to scope["user"]/scope["auth"], never setting the
current_principal/current_tenant/current_principal_metadata ContextVars.
auth_context_factory and adopter code reading current_principal.get()
directly got None on every A2A call even after successful auth, causing
AdCPAuthenticationError on policies that require a bound principal.

Mirrors the existing BearerTokenAuthMiddleware try/finally pattern:
set all three vars on auth success; set to None on OPTIONS/discovery
pass-throughs; reset unconditionally in finally.

Fixes #590

https://claude.ai/code/session_01ETystgT4HH4ZHaAziszjBF
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.

A2A: current_principal contextvar is None inside platform handlers (current_tenant works) — same token works on MCP

2 participants