A2A Security Implementation: Authentication, Authorization, and In-Task Auth

A2A Security Implementation: Authentication, Authorization, and In-Task Auth

Security implementation has two independent lines: letting legitimate callers prove who they are (authentication), and keeping every operation inside the caller’s permission boundary (authorization). The specification defines the requirements but does not pick a scheme for you; this guide turns them into an implementation order and negative probes you can run directly.

For field meanings see the SecurityScheme object page ; the complete normative requirements live in Authentication, Authorization and Security Considerations ; what to check before a release is the checklist in section 2 of the production readiness guide . This page answers “how to implement it and how to self-test it”.

Threat model

AssetAttackBoundary the protocol givesWhere it lands here
Public Agent CardDeclaration does not match the implementation, luring clients into sending credentialssecuritySchemes and securityRequirements must agreeSection 1
Task dataUnauthenticated calls reading tasks outrightRejected with binding-native error codesSection 2
Task dataA valid identity reading someone else’s tasksResults and operations stay inside the caller’s boundarySection 3
Task executionAn agent harvesting credentials along a task chainEntering TASK_STATE_AUTH_REQUIRED authorizes nothingSection 4
Extended cardUnauthenticated parties obtaining privileged informationGetExtendedAgentCard must require authenticationSection 5

1. Declaring security schemes

The Agent Card defines schemes in securitySchemes and states which are actually enabled in securityRequirements. A mismatch is the first thing card validation rejects: every scheme name referenced by securityRequirements must exist in securitySchemes.

{
  "securitySchemes": {
    "oauth": { "type": "oauth2", "flows": { "authorizationCode": { "authorizationUrl": "https://idp.example.com/authorize", "tokenUrl": "https://idp.example.com/token", "scopes": { "tasks:read": "Read tasks", "tasks:write": "Create and cancel tasks" } } } }
  },
  "securityRequirements": [ { "schemes": { "oauth": { "list": [ "tasks:read", "tasks:write" ] } } } ]
}

Scheme types and field details (the five types, the OAuth flow table, the v0.3 renames) are on the SecurityScheme object page . Two common traps: writing secrets into the public card, and leaving the v0.3 security field unmigrated — the name changed to securityRequirements in v1.0.

2. Answering authentication on the three bindings

Authentication failures are not A2A-specific errors; each binding uses its own native codes. The “beyond the nine” table in the error reference maps them:

BindingAuthentication failureAuthorization failureWhere the challenge travels
HTTP+JSON401403WWW-Authenticate response header
JSON-RPC over HTTP401 (rejected at the HTTP layer) or a JSON-RPC custom error403 or a custom errorSame as HTTP; credentials ride the Authorization header, never the JSON-RPC params
gRPCUNAUTHENTICATEDPERMISSION_DENIEDwww-authenticate metadata; credentials ride gRPC metadata

Implementation notes:

  1. Credentials travel on the transport (HTTP header or gRPC metadata), never inside message bodies or task parameters;
  2. A 401 must carry a structurally correct challenge naming the scheme to use — it corresponds to an entry in the card’s securitySchemes;
  3. When validating tokens, check issuer, audience, and expiry together; any failure rejects as unauthenticated without running agent work;
  4. Log the scheme name and failure reason for rejected challenges, never the token itself.

3. Where authorization checks live

Authentication answers “who”; authorization answers “which tasks may this identity touch”. Server-side, the check belongs in each operation’s data query, not in after-the-fact filtering:

OperationThe query must include
GetTask / CancelTask / SubscribeToTaskCaller identity + task ID, filtered together
ListTasksScoped to the caller (and the tenant of the chosen AgentInterface), paginated
Push notification config CRUDOnly after task ownership is verified
Extended cardAuthentication level + content tiering

The specification requires that unauthorized callers must not be able to distinguish “task does not exist” from “no access”. Returning 404 (HTTP) / NOT_FOUND (gRPC) uniformly is safer than leaking existence through a 403. The cross-user and cross-tenant test scenarios are the checklist in section 2 of the production readiness guide .

4. Implementing in-task authorization (TASK_STATE_AUTH_REQUIRED)

When execution needs a new credential (say an OAuth token for a downstream API), the protocol’s fallback is moving the task into TASK_STATE_AUTH_REQUIRED. Implementation checklist:

  1. Track the operation with a Task; the state transition must actually happen in the task state machine;
  2. State the required authorization in the TaskStatus message, unless the details were negotiated out of band;
  3. Keep the response stream open and resume immediately when the credential arrives out of band;
  4. Keep accepting messages sent to the task — the client may negotiate, correct, or decline;
  5. Receive credentials over an out-of-band secure channel; when they must travel along an agent chain, bind them to the originating agent and encrypt sensitive ones for everyone else on the chain;
  6. The state transition itself grants nothing — entering and leaving AUTH_REQUIRED does not approve any specific operation; the scope of the grant is defined by your implementation or the credential issuer.

The full protocol semantics (both parties’ duties) are under In-task authorization . Clients avoid missing updates by subscribing, registering push, or polling — pick one.

5. Access control for the extended card

GetExtendedAgentCard must require authentication. You may return different content per authentication level (extra skills, finer quotas, organization-specific configuration), but document what each level can see; verify permissions before returning privileged information, and keep “dangerous if leaked” content (internal URLs, unredacted credentials) out of the extended card. Declared but unconfigured returns ExtendedAgentCardNotConfiguredError; undeclared returns UnsupportedOperationError.

6. Self-test: negative probes

The script below fires four classes of negative request at a public endpoint. Run it next to the public preflight in section 1 of the production readiness guide : the preflight proves the card is publicly consistent; the probes prove the endpoint actually rejects bad requests.

#!/usr/bin/env bash
set -euo pipefail

: "${A2A_BASE_URL:?Set A2A_BASE_URL, e.g. https://agent.example.com/}"
: "${A2A_VALID_TOKEN:?Set A2A_VALID_TOKEN to a token accepted by the agent}"

probe() { # label expected_code curl args...
  local label="$1" expected="$2"; shift 2
  local code
  code="$(curl --silent --output /dev/null --write-out '%{http_code}' "$@")"
  if [[ "$code" == "$expected" ]]; then
    printf 'OK   %s -> %s\n' "$label" "$code"
  else
    printf 'FAIL %s -> %s (expected %s)\n' "$label" "$code" "$expected" >&2
    exit 1
  fi
}

BODY='{"jsonrpc":"2.0","id":1,"method":"message/send","params":{"message":{"role":"role_user","parts":[{"kind":"text","text":"hi"}],"messageId":"probe-'$$'"}}}'

probe "no credential"       401 -H 'Content-Type: application/json' -d "$BODY" "$A2A_BASE_URL"
probe "malformed token"     401 -H 'Authorization: Bearer not-a-jwt' -H 'Content-Type: application/json' -d "$BODY" "$A2A_BASE_URL"
probe "foreign task read"   404 -H "Authorization: Bearer $A2A_VALID_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"jsonrpc":"2.0","id":2,"method":"tasks/get","params":{"taskId":"00000000-0000-0000-0000-000000000000"}}' \
  "$A2A_BASE_URL"

# The 401 must carry a usable challenge, not just the status code.
challenge="$(curl --silent --dump-header - --output /dev/null \
  -H 'Content-Type: application/json' -d "$BODY" "$A2A_BASE_URL" \
  | tr -d '\r' | awk 'tolower($1)=="www-authenticate:" {$1=""; print; exit}')"
[[ -n "$challenge" ]] || { printf 'FAIL 401 without WWW-Authenticate challenge\n' >&2; exit 1; }
printf 'OK   401 carries challenge: %s\n' "$challenge"

Every line should print OK. The probes deliberately use a nonexistent task ID: a correct implementation answers 404, indistinguishable from “no access to someone else’s real task”. For a gRPC endpoint, assert UNAUTHENTICATED and NOT_FOUND statuses with the same scenarios.

7. Observability and credential rotation

  • Logging: record the scheme name, failure reason, and a source fingerprint (e.g. caller_subject_hash) for authentication failures — never the credential itself; count authorization denials separately from authentication failures;
  • Alerts: authentication-failure rate, one identity probing across tenants, unusual request volume on the extended card;
  • Rotation: publish the new verification key (dual read) → switch issuance → revoke the old key. Push notification tokens follow the same rhythm, one single-purpose token per config (see Push notification security ).

Related pages

Version and verification notes

Aligned with A2A v1.0.1. The probe script depends on curl; names such as role_user, messageId, and tasks/get follow the proto and the data structures . The probes were verified against an example endpoint for behavior classes (401/404/challenge header); no specific identity provider’s token issuance is covered.

Sources: A2A Authentication, Authorization and Security Considerations , A2A error codes .