Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ This README serves both sides of an AdCP integration. Jump to what you're doing:
- [Building an AdCP Agent](#building-an-adcp-agent)
- [Multi-agent discovery manifest](#multi-agent-discovery-manifest)
- [Connecting to AdCP Agents](#connecting-to-adcp-agents)
- [Buyer OAuth authorization code with PKCE](#buyer-oauth-authorization-code-with-pkce)
- [The Core Concept](#the-core-concept)
- [Installation](#installation)
- [Quick Start: Test Helpers](#quick-start-test-helpers)
Expand Down Expand Up @@ -141,6 +142,61 @@ serve(

## Connecting to AdCP Agents

### Buyer OAuth authorization code with PKCE

`adcp.oauth` provides a hardened, pre-registered **public-client** flow. Pass a
trusted authorization-server issuer URL (not an MCP resource URL), bind `state`
to the user's browser session, and keep pending flows server-side:

```python
from adcp.oauth import (
InMemoryPendingOAuthFlowStore,
OAuthIssuerBinding,
complete_oauth_authorization,
discover_oauth_metadata,
start_oauth_authorization,
)

pending = InMemoryPendingOAuthFlowStore() # development / one process only
metadata = await discover_oauth_metadata("https://login.example.com/tenant")

request = await start_oauth_authorization(
metadata,
client_id="registered-public-client-id",
redirect_uri="https://buyer.example.com/oauth/callback",
store=pending,
issuer_binding=OAuthIssuerBinding.AUTHORIZATION_RESPONSE_ISS,
scopes=["media.buy"],
resource="https://seller.example.com/mcp",
)
# Store request.state in the authenticated browser session, then redirect the
# browser to request.authorization_url.

tokens = await complete_oauth_authorization(
code=callback_query.get("code"),
callback_state=callback_query["state"],
expected_state=browser_session["oauth_state"],
callback_issuer=callback_query.get("iss"),
store=pending,
)
bearer = tokens.access_token.get_secret_value()
```

Discovery requires an exact RFC 8414 issuer match, S256 PKCE, authorization
code support, and `token_endpoint_auth_methods_supported: ["none", ...]`.
Metadata and token requests are size-bounded, ignore proxy environment
variables, reject redirects/compression, and use DNS-pinned transports. For an
authorization server without RFC 9207 `iss` support, use
`DISTINCT_REDIRECT_URI` only when that callback URI is exclusive to one issuer.
Multi-process deployments must implement `PendingOAuthFlowStore` using shared
storage with atomic insert-if-absent and consume operations. Encrypt the real
`SecretStr` verifier value at rest; JSON-serializing the model produces the
masked display value, not a recoverable verifier. Once completion consumes a
flow, any failure or cancellation requires starting a new one instead of
retrying it. Plain HTTP is disabled by default; `allow_loopback_http=True` is a
development/native app escape limited to literal loopback IPs and is persisted
with the flow.

## The Core Concept

AdCP operations are **distributed and asynchronous by default**. An agent might:
Expand Down
50 changes: 50 additions & 0 deletions src/adcp/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,23 @@ def _resolve_version() -> str:
"inject_trace_headers",
"is_tracing_available",
),
"adcp.oauth": (
"InMemoryPendingOAuthFlowStore",
"OAuthAuthorizationError",
"OAuthAuthorizationRequest",
"OAuthAuthorizationServerMetadata",
"OAuthClientError",
"OAuthDiscoveryError",
"OAuthFlowStoreError",
"OAuthIssuerBinding",
"OAuthTokenExchangeError",
"OAuthTokenSet",
"PendingOAuthAuthorization",
"PendingOAuthFlowStore",
"complete_oauth_authorization",
"discover_oauth_metadata",
"start_oauth_authorization",
),
"adcp.exceptions": (
"AdagentsAccessBlockedError",
"AdagentsNotFoundError",
Expand Down Expand Up @@ -857,6 +874,22 @@ def get_adcp_version() -> str:
"get_tracer",
"inject_trace_headers",
"is_tracing_available",
# Buyer OAuth authorization-code helpers
"InMemoryPendingOAuthFlowStore",
"OAuthAuthorizationError",
"OAuthAuthorizationRequest",
"OAuthAuthorizationServerMetadata",
"OAuthClientError",
"OAuthDiscoveryError",
"OAuthFlowStoreError",
"OAuthIssuerBinding",
"OAuthTokenExchangeError",
"OAuthTokenSet",
"PendingOAuthAuthorization",
"PendingOAuthFlowStore",
"complete_oauth_authorization",
"discover_oauth_metadata",
"start_oauth_authorization",
"RegistryClient",
"PropertyRegistry",
"RegistrySync",
Expand Down Expand Up @@ -1559,6 +1592,23 @@ def get_adcp_version() -> str:
FeedStateStore,
RefreshResult,
)
from adcp.oauth import (
InMemoryPendingOAuthFlowStore,
OAuthAuthorizationError,
OAuthAuthorizationRequest,
OAuthAuthorizationServerMetadata,
OAuthClientError,
OAuthDiscoveryError,
OAuthFlowStoreError,
OAuthIssuerBinding,
OAuthTokenExchangeError,
OAuthTokenSet,
PendingOAuthAuthorization,
PendingOAuthFlowStore,
complete_oauth_authorization,
discover_oauth_metadata,
start_oauth_authorization,
)
from adcp.observability import get_tracer, inject_trace_headers, is_tracing_available
from adcp.property_registry import PropertyRegistry
from adcp.registry import RegistryClient
Expand Down
Loading
Loading