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
| Asset | Attack | Boundary the protocol gives | Where it lands here |
|---|---|---|---|
| Public Agent Card | Declaration does not match the implementation, luring clients into sending credentials | securitySchemes and securityRequirements must agree | Section 1 |
| Task data | Unauthenticated calls reading tasks outright | Rejected with binding-native error codes | Section 2 |
| Task data | A valid identity reading someone else’s tasks | Results and operations stay inside the caller’s boundary | Section 3 |
| Task execution | An agent harvesting credentials along a task chain | Entering TASK_STATE_AUTH_REQUIRED authorizes nothing | Section 4 |
| Extended card | Unauthenticated parties obtaining privileged information | GetExtendedAgentCard must require authentication | Section 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:
| Binding | Authentication failure | Authorization failure | Where the challenge travels |
|---|---|---|---|
| HTTP+JSON | 401 | 403 | WWW-Authenticate response header |
| JSON-RPC over HTTP | 401 (rejected at the HTTP layer) or a JSON-RPC custom error | 403 or a custom error | Same as HTTP; credentials ride the Authorization header, never the JSON-RPC params |
| gRPC | UNAUTHENTICATED | PERMISSION_DENIED | www-authenticate metadata; credentials ride gRPC metadata |
Implementation notes:
- Credentials travel on the transport (HTTP header or gRPC metadata), never inside message bodies or task parameters;
- A
401must carry a structurally correct challenge naming the scheme to use — it corresponds to an entry in the card’ssecuritySchemes; - When validating tokens, check issuer, audience, and expiry together; any failure rejects as unauthenticated without running agent work;
- 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:
| Operation | The query must include |
|---|---|
GetTask / CancelTask / SubscribeToTask | Caller identity + task ID, filtered together |
ListTasks | Scoped to the caller (and the tenant of the chosen AgentInterface), paginated |
| Push notification config CRUD | Only after task ownership is verified |
| Extended card | Authentication 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:
- Track the operation with a Task; the state transition must actually happen in the task state machine;
- State the required authorization in the
TaskStatusmessage, unless the details were negotiated out of band; - Keep the response stream open and resume immediately when the credential arrives out of band;
- Keep accepting messages sent to the task — the client may negotiate, correct, or decline;
- 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;
- The state transition itself grants nothing — entering and leaving
AUTH_REQUIREDdoes 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
- SecurityScheme object page : the five scheme types and the OAuth flow table
- Error code reference : the boundary between binding-native and A2A-specific errors
- Production readiness guide : the section 2 checklist for authentication and isolation
- Push receiver hardening : the outbound side of security
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 .