Skip to content

Commit 078d0aa

Browse files
committed
fix(server): auth_context_factory populates adcp.auth_info for bearer flows
Closes #576 (partial — handler.py pop follows in next commit) https://claude.ai/code/session_01R254Wmibw6fxiDDTvhuw9b
1 parent 6c49b31 commit 078d0aa

2 files changed

Lines changed: 67 additions & 1 deletion

File tree

src/adcp/server/auth.py

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -400,16 +400,44 @@ def auth_context_factory(meta: RequestMetadata) -> ToolContext:
400400
:class:`ToolContext` — agents that want a typed subclass
401401
(e.g. :class:`~adcp.server.AccountAwareToolContext`) should copy
402402
the three-line body and return their own subclass instead.
403+
404+
Also sets ``metadata["adcp.auth_info"]`` to a typed
405+
:class:`~adcp.decisioning.AuthInfo` when the request is
406+
authenticated, so :meth:`~adcp.decisioning.PlatformHandler._extract_auth_info`
407+
surfaces a non-``None`` :attr:`~adcp.decisioning.RequestContext.auth_info`
408+
for bearer flows — the same typed surface signed-request flows already
409+
populate. ``credential`` is ``None`` for bearer flows because inbound
410+
bearer tokens are not for upstream propagation; adopters who need
411+
:class:`~adcp.decisioning.BuyerAgentRegistry` dispatch must supply a
412+
typed credential in a custom ``context_factory`` subclass.
413+
414+
``adcp.auth_info`` is server-internal and never wire-echoed by the
415+
framework. Do not pass ``ctx.metadata`` wholesale to a JSON serializer
416+
— the ``AuthInfo`` object is not JSON-serializable.
403417
"""
418+
principal_identity = current_principal.get()
404419
principal_metadata = current_principal_metadata.get() or {}
405420
combined_metadata: dict[str, Any] = {
406421
**principal_metadata,
407422
"tool_name": meta.tool_name,
408423
"transport": meta.transport,
409424
}
425+
if principal_identity is not None:
426+
# Lazy import to keep module-load order safe — decisioning.context
427+
# imports adcp.server.base but not adcp.server.auth, so there is no
428+
# circular dependency, but hoisting this to module level would create
429+
# one if the import graph ever changes. Call-time import matches
430+
# the pattern already used in dispatch._build_request_context.
431+
from adcp.decisioning.context import AuthInfo # noqa: PLC0415
432+
433+
combined_metadata["adcp.auth_info"] = AuthInfo(
434+
kind="bearer",
435+
principal=principal_identity,
436+
credential=None, # explicit None: no synthesis, no DeprecationWarning
437+
)
410438
return ToolContext(
411439
request_id=meta.request_id,
412-
caller_identity=current_principal.get(),
440+
caller_identity=principal_identity,
413441
tenant_id=current_tenant.get(),
414442
metadata=combined_metadata,
415443
)

tests/test_auth_middleware.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -562,6 +562,44 @@ def test_auth_context_factory_with_no_principal() -> None:
562562

563563
assert ctx.caller_identity is None
564564
assert ctx.tenant_id is None
565+
assert "adcp.auth_info" not in (ctx.metadata or {})
566+
567+
568+
def test_auth_context_factory_populates_auth_info_when_authenticated() -> None:
569+
"""auth_context_factory must set ctx.metadata['adcp.auth_info'] to a typed
570+
AuthInfo(kind='bearer') when a principal is present, so ctx.auth_info is
571+
non-None for bearer flows in downstream RequestContext. Regression guard
572+
for issue #576."""
573+
from adcp.decisioning.context import AuthInfo
574+
from adcp.server import RequestMetadata
575+
576+
principal_token = current_principal.set("alice")
577+
tenant_token = current_tenant.set("t1")
578+
try:
579+
meta = RequestMetadata(tool_name="get_products", transport="mcp")
580+
ctx = auth_context_factory(meta)
581+
finally:
582+
current_principal.reset(principal_token)
583+
current_tenant.reset(tenant_token)
584+
585+
info = ctx.metadata.get("adcp.auth_info")
586+
assert isinstance(info, AuthInfo), f"expected AuthInfo, got {type(info)}"
587+
assert info.kind == "bearer"
588+
assert info.principal == "alice"
589+
assert info.credential is None # inbound tokens are not for upstream propagation
590+
591+
592+
def test_auth_context_factory_omits_auth_info_without_principal() -> None:
593+
"""Non-discovery requests with no principal (principal=None) must NOT set
594+
adcp.auth_info in metadata — the key is only set when authenticated."""
595+
from adcp.server import RequestMetadata
596+
597+
# Use a non-discovery tool so this test is distinct from
598+
# test_auth_context_factory_with_no_principal above.
599+
meta = RequestMetadata(tool_name="get_products", transport="mcp")
600+
ctx = auth_context_factory(meta)
601+
602+
assert "adcp.auth_info" not in (ctx.metadata or {})
565603

566604

567605
# Full-stack composition (middleware + create_mcp_server + handler) is

0 commit comments

Comments
 (0)