The Agent2Agent protocol deliberately does not invent its own security. Authentication happens at the HTTP layer with mechanisms enterprises already run: OAuth 2.0 bearer tokens, OpenID Connect, API keys and mutual TLS. The agent's card declares which ones it accepts, the client obtains credentials out of band and sends them in headers on every request, and the server authenticates every request before doing anything else. That division is simple to state and easy to get wrong in code, because three different parties end up holding credentials: the calling service, the user it acts for, and the webhook that receives push notifications.

This page covers those patterns for ADK Java: declaring requirements in the card, attaching credentials from a RemoteA2AAgent (including a trap that sends requests with no credentials at all), validating callers on the server, delegating a user's authority, authenticating push notifications, failure modes and a checklist. Verifying that a card really belongs to the agent you meant to call is a separate problem, covered in A2A identity verification.

Which protocol version you are speaking

First, know which protocol version your stack speaks, because field names changed. As of 2026-10-08 the google-adk-a2a module on adk-java main pins the a2a-java SDK at 0.3.2.Final, with io.a2a packages and A2A 0.3 names on the wire. The a2a-java main branch has moved to the 1.x line under org.a2aproject.sdk and implements A2A 1.0. Code on this page uses the 0.3 names that ADK Java ships with; the mapping matters when your agents talk to 1.0 peers.

ConceptA2A 0.3 (ADK Java today)A2A 1.0
Accepted credential combinationssecuritysecurityRequirements
Scheme definition{"type": "oauth2", ...}{"oauth2SecurityScheme": {...}}
Mutual TLS schemetype "mutualTLS"mtlsSecurityScheme
Extended cardsupportsAuthenticatedExtendedCard, agent/getAuthenticatedExtendedCardcapabilities.extendedAgentCard, GetExtendedAgentCard
Task needs authorizationauth-requiredTASK_STATE_AUTH_REQUIRED

Declaring requirements in the Agent Card

The card's securitySchemes map names each mechanism, following OpenAPI's security scheme object, and security lists which combinations are accepted. It is an OR of ANDs: each entry in the list is one acceptable set, and every scheme inside an entry is required together.

"securitySchemes": {
  "svc_oauth": {
    "type": "oauth2",
    "flows": { "clientCredentials": {
      "tokenUrl": "https://idp.example.com/oauth2/token",
      "scopes": { "invoices.read": "Read invoices", "invoices.dispute": "Open disputes" } } }
  },
  "mtls": { "type": "mutualTLS" }
},
"security": [
  { "svc_oauth": ["invoices.read"], "mtls": [] }
]

This card says: present a client-credentials token with invoices.read and connect over mutual TLS. The card is public, so it should describe how to authenticate, not hold anything secret. Skills or details you only want to show authenticated callers belong in the extended card, which the server must itself protect with one of the declared schemes.

Three credentials, three boundaries

Three credentials in one delegation: service, user, and webhookOrchestratorADK LlmAgentRemoteA2AAgentA2A client + interceptorRemote A2A servervalidates every requestIts toolsdownstream APIsBearer tokenIdentity providerclient credentialsToken exchangeuser context, new audiencefetch, cacheClient webhookverifies push credentialPush senderon task updateauthentication.credentialsEach arrow that crosses a trust boundary carries its own credential, scoped to its own audience.
Service credential to the remote agent, a separate exchanged token when acting for a user, and a third credential on push notifications.

Keep three credentials distinct in your design. The service credential proves which calling agent is making the request. A user credential proves on whose behalf, when the remote agent touches user data. The push credential proves to your webhook that a notification really came from the agent you registered it with. Collapsing any two of them is where most designs leak.

Attaching credentials from ADK Java

On the client side, a2a-java attaches headers with a ClientCallInterceptor. The SDK's AuthInterceptor reads the card's security list, asks a CredentialService for a credential per scheme name, and writes a header. You register it on the transport configuration and hand the built client to RemoteA2AAgent:

CredentialService creds = (schemeName, callContext) ->
        "svc_oauth".equals(schemeName) ? tokenCache.currentToken() : null;

Client client = Client.builder(agentCard)
        .withTransport(JSONRPCTransport.class,
                new JSONRPCTransportConfigBuilder().addInterceptor(new AuthInterceptor(creds)))
        .build();

RemoteA2AAgent invoices = RemoteA2AAgent.builder()
        .name("invoice_agent")
        .agentCard(agentCard)
        .a2aClient(client)
        .build();

The credential service above ignores the call context on purpose, and that is the trap worth knowing. The SDK ships InMemoryContextCredentialService, which looks up credentials by a sessionId entry in the per-call ClientCallContext and returns null when there is no context. On adk-java main as of 2026-10-08, RemoteA2AAgent calls sendMessage with a null context. Wire the in-memory service into an ADK agent and every request goes out with no Authorization header; the remote server answers 401, and the cause looks like a token problem when it is a plumbing one. Service credentials should therefore come from a context-free provider like the one above.

Two more behaviours of the 0.3.2 interceptor matter. It returns after the first scheme for which it finds a credential, so an AND requirement of two header-based schemes gets only one header; mutual TLS is unaffected because it lives in the TLS layer, not a header. And for API key schemes it writes a header named by the scheme without checking whether the card said the key belongs in a query parameter or cookie. If your peers declare either case, write your own interceptor.

The token cache does the client-credentials grant, keeps the token until shortly before its expiry, refreshes with a single in-flight request so a burst of calls does not stampede the identity provider, and drops the token on a 401 so the next call fetches a fresh one. Keep the client secret in a secret manager, as described in secrets management.

Mutual TLS

Mutual TLS proves the caller holds a private key for a certificate your CA issued, and there is no bearer token to steal from a log. The cost is certificate infrastructure: issuance, rotation and revocation for every calling service. Most teams terminate it in a service mesh or gateway rather than in the JVM. If you do, the proxy passes the verified client identity to the application in a header, and the application must accept that header only from the proxy: strip any inbound copy at the edge, or an external caller can simply claim to be another service. Mutual TLS combines well with OAuth: the certificate proves the workload, the token carries scopes, and sender-constrained tokens bind the two.

Acting for a user

When the remote agent reads or changes a user's data, the service credential is not enough; the remote side must know which user, and must check that user's rights. The tempting shortcut is forwarding the user's own access token. Do not: that token's audience is your front end, so the remote agent would either reject it or, worse, accept a token minted for someone else, and it now holds a credential it could replay against any service that trusts the same audience.

Use OAuth 2.0 token exchange (RFC 8693) instead. The orchestrator presents the user's token to the identity provider and receives a new one whose audience is the remote agent only, with narrower scopes and a short lifetime, and which records both the user and the acting service. The remote agent authorizes against the user in that token. Because this credential is per user and per call, it is the one case where a context-aware credential service is right, which means calling the A2A client yourself with a context rather than through RemoteA2AAgent until its call path carries one.

A2A also defines in-task authorization: a remote agent that discovers mid-task it needs consent moves the task to auth-required (TASK_STATE_AUTH_REQUIRED in 1.0) and explains what it needs. The A2A 1.0 specification spells out the rules: the credential should be delivered out of band, directly to the agent that asked, rather than passed back through the chain of agents, and entering that state is not by itself an authorization for anything.

Validating callers on the server

The server must authenticate every request, including task reads, cancellations and push configuration calls, and must scope results to the caller: a valid token for tenant A must never read tenant B's task by guessing its id. Whatever framework you host the A2A server in, the check runs before the agent executor sees the message:

AuthResult authenticate(HttpHeaders h, String skillId) {
    String token = bearer(h).orElseThrow(() -> Unauthorized.challenge("Bearer"));   // 401
    Jwt jwt = verifier.verify(token);          // signature against pinned JWKS, exp/nbf with small skew
    require(jwt.issuer().equals(TRUSTED_ISSUER));
    require(jwt.audience().contains(MY_AGENT_AUDIENCE));                            // not someone else's token
    Set<String> needed = SKILL_SCOPES.getOrDefault(skillId, Set.of("agent.invoke"));
    if (!jwt.scopes().containsAll(needed)) throw Forbidden.missing(needed);          // 403
    return new AuthResult(jwt.subject(), jwt.claim("act"), jwt.claim("tenant"));
}

Return 401 for missing or invalid credentials, with a challenge header, and 403 when the caller is known but not allowed; a client can retry the first with a fresh token, never the second. Pass the resulting identity to the agent through session state or the invocation's user id, so tools authorize against the real caller. Policy beyond scopes belongs in boundary authorization.

Authenticating push notifications

Push notifications reverse the direction: the remote agent calls your webhook. When registering, the client supplies a PushNotificationConfig with the URL, an optional token the agent echoes back, and an authentication object with schemes and credentials that the agent must present. Your webhook verifies that credential on every delivery, checks that the task id is one you created, and processes deliveries idempotently because retries duplicate them. Use a credential minted for this webhook alone, never your service token, since you are handing it to another party. On the sending side, validate registered URLs and refuse private, loopback and link-local addresses, or a caller can make your agent probe your internal network.

Failure modes

  • Silent unauthenticated calls. A context-keyed credential service behind RemoteA2AAgent yields no header and a 401. Use a context-free provider for service credentials, and test that the header is present.
  • Token stampede on expiry. Every call refreshes at once; single-flight the refresh and renew early.
  • Forwarded user tokens. Wrong audience, replayable elsewhere; use token exchange.
  • Missing task scoping. Authenticated callers read other tenants' tasks by id.
  • Spoofable identity header behind a mesh that does not strip inbound copies.
  • Version mismatch. A 0.3 client may fail to parse a 1.0 peer's card, and if it does parse it there is no security field, so the interceptor attaches nothing; pin versions or translate cards.

Trade-offs

Client credentials over TLS is the cheapest pattern that is actually secure: every language has a library for it, tokens expire on their own, and scopes give the server something to authorize against. Its weakness is that a bearer token works for whoever holds it, so a token copied from a log or a proxy is usable until it expires; short lifetimes limit the window. Mutual TLS removes the bearer problem but adds a certificate lifecycle you must run for every workload, which is why it usually lives in a mesh. API keys are the simplest of all and the weakest: no expiry, no scopes, and rotation means coordinating every caller, so reserve them for low-risk internal agents. Token exchange adds an identity-provider round trip per delegated call and a dependency on that provider being available, in return for remote agents that never see a reusable user credential. Most production deployments combine two: a mesh for workload identity and OAuth tokens for scopes and users.

What to do next

  1. Check which A2A version each of your agents and peers speaks, and record it next to the card.
  2. Declare every accepted scheme in securitySchemes and security; serve nothing anonymously.
  3. Give each calling agent its own client-credentials identity and a context-free credential provider with a single-flight token cache.
  4. Add a test that asserts the Authorization header is present on requests sent by RemoteA2AAgent.
  5. On the server, verify issuer, audience, expiry and per-skill scopes on every operation, and scope task reads by tenant.
  6. Replace any forwarded user token with RFC 8693 token exchange.
  7. Give push webhooks their own credential, verify it on every delivery, and guard registered URLs against SSRF.
  8. Read A2A error propagation so 401 and 403 surface to the orchestrator correctly.
Key takeaway: A2A puts authentication in HTTP: the card declares schemes, the client fetches credentials out of band and sends them on every request, and the server verifies every operation and scopes it to the caller. In ADK Java, give RemoteA2AAgent a context-free credential provider, because it calls the client without a call context. Keep service, user and webhook credentials separate, exchange user tokens rather than forwarding them, and know whether each peer speaks A2A 0.3 or 1.0.