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 in authdog/agents-samples. Python 3.10+. Package a2a-sdk[http-server]==1.2.3.
Channel auth, machine credentials, tool checks, and instructions for this framework are the other pages under A2A in AI.
Channel auth, machine credentials, tool checks, and instructions for this framework are the other pages under A2A in AI.
Channel auth, machine credentials, tool checks, and instructions for this framework are the other pages under A2A in AI.
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.
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.
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
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
GET /.well-known/agent-card.jsonwith no token returns 200 and includes the token URL andagent:invoke.POST /a2a/withA2A-Version: 1.0and no bearer returns401and{"error":"Unauthorized"}, plusWWW-Authenticate: Bearer. The body does not containsubject:.- 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. |
Related
| Read | To learn how to |
|---|---|
| Agent frameworks | See the same bearer on the other four hooks |
| Eve | Move the check onto a channel auth walk |
| Machine identity | Create the confidential client |