An A2A agent names Authdog on its public agent card, then admits message calls only after `require_auth` accepts the bearer. The client exchanges `client_credentials` out of band and sends the token on the HTTP request. The JSON-RPC body stays the message.

Sample: [a2a/python](https://github.com/authdog/agents-samples/tree/main/a2a/python) in [authdog/agents-samples](https://github.com/authdog/agents-samples). Python 3.10+. Package `a2a-sdk[http-server]==1.2.3`.

## Two surfaces

| Surface | Auth |
| --- | --- |
| `GET /.well-known/agent-card.json` | Public. Names the token URL and scope `agent:invoke`. |
| `POST /a2a/` | `authdog.starlette` `require_auth` before the executor runs. |

The card is how a client discovers the scheme. The middleware is the gate. Publishing the card does not protect `SendMessage`.

## Declare the scheme

Build an OAuth 2 client-credentials scheme whose token URL is the environment `token_endpoint`. Attach it as security scheme `authdog` with scope `agent:invoke`.

```python
flow = ClientCredentialsOAuthFlow(token_url=token_url)
flow.scopes["agent:invoke"] = "Call this agent"
scheme = SecurityScheme(
    oauth2_security_scheme=OAuth2SecurityScheme(
        description="Authdog agent identity (client credentials)",
        flows=OAuthFlows(client_credentials=flow),
    )
)
requirement = SecurityRequirement()
requirement.schemes["authdog"].list.append("agent:invoke")
```

Pass `security_schemes={"authdog": scheme}` and `security_requirements=[requirement]` on the `AgentCard`. The token URL is public configuration. The client secret never goes on the card.

## Gate the JSON-RPC mount

Construct `Authdog(public_key=...)` from `PK_AUTHDOG`. Wrap the `/a2a` routes in middleware that calls `require_auth` and copies the subject onto the request before the A2A handler runs.

```python
class _Gate(BaseHTTPMiddleware):
    async def dispatch(self, request: Request, call_next):
        try:
            user = await authdog.require_auth(request)
        except HTTPException:
            return JSONResponse(
                {"error": "Unauthorized"},
                status_code=401,
                headers={"WWW-Authenticate": "Bearer"},
            )
        request.state.authdog_subject = subject_of(user)
        return await call_next(request)
```

`subject_of` reads `user["id"]`, then `user["sub"]`. A custom A2A context builder copies `authdog_subject` into `context.call_context.state`. The executor replies with `subject:<id>` and does not look at the raw `Authorization` header.

`SendMessage` requires the `A2A-Version: 1.0` header. Without it the SDK reports that 0.3 is not supported, which is a protocol error, separate from the 401.

## Call it

```python
response = await http.post(
    token_url,
    data={"grant_type": "client_credentials"},
    auth=(client_id, client_secret),
    headers={"Content-Type": "application/x-www-form-urlencoded"},
)
token = response.json()["access_token"]

async with httpx.AsyncClient(
    headers={"Authorization": f"Bearer {token}"},
) as http:
    card = await A2ACardResolver(http, base_url).get_agent_card()
    client = ClientFactory(
        ClientConfig(httpx_client=http, streaming=False)
    ).create(card)
```

The same HTTP client fetches the card and sends the message, so the bearer rides on both. Card fetch succeeds without a token. Message calls do not.

Mint again when `expires_in` is close. The sample client performs one exchange per run.

## What you should see

1. `GET /.well-known/agent-card.json` with no token returns 200 and includes the token URL and `agent:invoke`.
2. `POST /a2a/` with `A2A-Version: 1.0` and no bearer returns `401` and `{"error":"Unauthorized"}`, plus `WWW-Authenticate: Bearer`. The body does not contain `subject:`.
3. The same call with a token userinfo accepts returns a message whose text is `subject:<id>`.

## Configure

| Variable | Process | Purpose |
| --- | --- | --- |
| `PK_AUTHDOG` | Server | Public key. Selects the identity host. |
| `AUTHDOG_TOKEN_URL` | Server and client | Token URL written on the card and used for the exchange. |
| `A2A_PUBLIC_URL` | Server | URL advertised on the card for the JSON-RPC binding. |
| `AUTHDOG_CLIENT_ID` / `AUTHDOG_CLIENT_SECRET` | Client | Agent credentials. Server does not read them. |
| `A2A_BASE_URL` | Client | Origin the client resolves the card from. |

> **Caution**
>
> Install `authdog-fastapi` from the pinned [web-sdk](https://github.com/authdog/web-sdk) commit in the sample `requirements.txt`, with the `starlette` extra. The server imports `authdog.starlette.Authdog`.

## Related

| Read | To learn how to |
| --- | --- |
| [Agent frameworks](/docs/agent-frameworks) | See the same bearer on the other four hooks |
| [Eve](/docs/agent-frameworks/eve) | Move the check onto a channel auth walk |
| [Machine identity](/docs/machine-identity) | Create the confidential client |
