Authdog

A2A

Last updated Oct 20, 2026
View as Markdown

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.

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

  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.
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

Learn more