A2A 安全实现:认证、授权与任务内授权
安全实现有两条独立的线:让合法调用方证明自己是谁(认证),以及让每个操作只碰自己权限边界内的任务(授权)。协议规范定义了要求,但不替您选方案;这里把要求翻译成实现顺序和可以直接运行的负向探针。
字段含义见 SecurityScheme 对象页 ;规范原文的完整要求见认证、授权与安全注意事项 ;发布前要检查哪些项,见生产就绪检查 第 2 节的检查表。本文回答"怎么实现、怎么自测"。
威胁模型
| 资产 | 攻击 | 协议给的边界 | 实现落点(本文小节) |
|---|---|---|---|
| 公开 Agent Card | 声明与实现不符,诱导客户端发送凭据 | securitySchemes + securityRequirements 必须互相对得上 | 第 1 节 |
| 任务数据 | 未认证调用直接读任务 | 认证失败用绑定原生错误码拒绝 | 第 2 节 |
| 任务数据 | 合法身份读取他人任务 | 结果与操作都限制在调用方边界内 | 第 3 节 |
| 任务执行 | 智能体借任务链套取凭据 | TASK_STATE_AUTH_REQUIRED 不构成对任何操作的授权 | 第 4 节 |
| 扩展卡片 | 未认证方获取特权信息 | GetExtendedAgentCard 必须认证 | 第 5 节 |
1. 声明安全方案
Agent Card 用 securitySchemes 定义方案、用 securityRequirements 声明"实际启用哪些方案"。两者对不上,是卡片校验最先失败的地方:securityRequirements 里引用的每个方案名都必须出现在 securitySchemes 中。
{
"securitySchemes": {
"oauth": { "type": "oauth2", "flows": { "authorizationCode": { "authorizationUrl": "https://idp.example.com/authorize", "tokenUrl": "https://idp.example.com/token", "scopes": { "tasks:read": "读取任务", "tasks:write": "创建与取消任务" } } } }
},
"securityRequirements": [ { "schemes": { "oauth": { "list": [ "tasks:read", "tasks:write" ] } } } ]
}方案类型与字段细节(五种类型、OAuth 流程表、v0.3 迁移改名)见 SecurityScheme 对象页
。两个常见陷阱:把机密写进公开卡片;security(v0.3 写法)没有迁移为 securityRequirements,老字段名在 v1.0 已改名。
2. 在三种绑定上应答认证
认证失败不是 A2A 专有错误,各绑定用它自己的原生错误码;错误码速查 的"九个之外"表列出了对应关系:
| 绑定 | 认证失败 | 授权失败 | 挑战的载体 |
|---|---|---|---|
| HTTP+JSON | 401 | 403 | WWW-Authenticate 响应头 |
| JSON-RPC over HTTP | 401(HTTP 层拒绝)或 JSON-RPC 自定义错误 | 403 或自定义错误 | 同 HTTP;请求凭据放 Authorization 头,不进 JSON-RPC 参数 |
| gRPC | UNAUTHENTICATED | PERMISSION_DENIED | www-authenticate 元数据;凭据放 gRPC metadata |
实现要点:
- 凭据在传输层携带(HTTP 头或 gRPC metadata),不要放进消息正文或任务参数;
401响应必须带结构正确的挑战,告诉客户端该用什么方案——它对应卡片里的某个securitySchemes条目;- 验证令牌时同时校验 issuer、audience、有效期;任何一项不过都按未认证拒绝,并且不执行智能体工作;
- 挑战失败的日志记录方案名与失败原因,不记录令牌本体。
3. 授权检查落点
认证回答"是谁",授权回答"这个身份能碰哪些任务"。在服务端,授权检查落在每个操作的数据查询上,而不是事后过滤:
| 操作 | 查询必须带上 |
|---|---|
GetTask / CancelTask / SubscribeToTask | 调用方身份 + 任务 ID 联合过滤 |
ListTasks | 按调用方(及 AgentInterface.tenant 的租户)分页 |
| 推送通知配置的增删改查 | 任务归属校验通过后才允许操作配置 |
| 扩展卡片 | 认证级别 + 返回内容的分级 |
规范要求:对未获授权的调用方不得区分"任务不存在"和"无权访问"。实现上统一返回 404(HTTP)/ NOT_FOUND(gRPC),比在 403 里泄漏存在性更安全。跨用户、跨租户的测试场景清单见生产就绪检查
第 2 节。
4. 实现任务内授权(TASK_STATE_AUTH_REQUIRED)
执行中需要新凭据(例如调用下游 API 的 OAuth 令牌)时,协议规定的兜底路径是把任务转入 TASK_STATE_AUTH_REQUIRED。实现清单:
- 用 Task 跟踪这次操作,状态转换必须真实发生在任务状态机里;
- 在
TaskStatus的消息中说明需要什么授权,除非细节已带外协商; - 保持响应流不断开,凭据带外到达后立即继续处理;
- 继续接收发给该任务的消息,客户端可能要协商、更正或拒绝;
- 凭据通过带外安全通道接收;沿智能体链传递时只绑定发起方可用,敏感凭据对链上其他方加密;
- 状态转换本身不构成授权——进入
AUTH_REQUIRED再退出,不代表任何具体操作被批准;授权范围由您的实现或凭据签发方定义。
完整的协议语义(智能体与客户端双方的职责)见任务内授权 。客户端一侧要避免错过更新:订阅、注册推送或轮询,三选一。
5. 扩展卡片的访问控制
GetExtendedAgentCard 必须要求认证。可以按认证级别返回不同内容(额外技能、更细的配额、组织专属配置),但要在卡片或文档里说明各级别能看到什么;返回特权信息前校验权限,扩展卡片里不放"泄露即危险"的内容(内部 URL、未脱敏凭据)。声明了能力却未配置时返回 ExtendedAgentCardNotConfiguredError,未声明时返回 UnsupportedOperationError。
6. 自测:负向探针
下面的脚本对公开端点执行四类负向请求。把它与生产就绪检查 第 1 节的公开预检放在一起运行:预检证明卡片是公开一致的,探针证明端点真的在拒绝坏请求。
#!/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"预期输出全部为 OK。探针刻意使用不存在的任务 ID:在正确实现里它应当得到 404,与"无权访问他人的真实任务"不可区分。gRPC 端点用相同场景断言 UNAUTHENTICATED 与 NOT_FOUND 状态码。
7. 观测与凭据轮换
- 日志:认证失败记方案名、失败原因、来源指纹(如
caller_subject_hash),不记凭据本体;授权拒绝与认证失败分开计数; - 告警:认证失败速率、同一身份跨租户探测模式、扩展卡片的异常请求量;
- 轮换:按"发布新验证密钥(双读)→ 切换签发 → 吊销旧密钥"三步走;推送通知配置里的令牌遵循同样节奏,每份配置一个单一用途令牌(见推送通知安全 )。
相关页面
- SecurityScheme 对象页 :五种方案类型与 OAuth 流程表
- 错误码速查 :绑定原生错误码与 A2A 专有错误的分界
- 生产就绪检查 :第 2 节的认证与隔离检查表
- 推送接收方加固 :出站方向的安全实现
版本与验证说明
对齐 A2A v1.0.1。探针脚本依赖 curl;role_user、messageId、tasks/get 等名称以 proto
与数据结构
为准。本文的探针在示例端点上验证过行为分类(401/404/挑战头),没有验证任何具体身份提供方的令牌签发。
来源:A2A 认证、授权与安全注意事项 、A2A 错误码 。