A2A 安全实现:认证、授权与任务内授权

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+JSON401403WWW-Authenticate 响应头
JSON-RPC over HTTP401(HTTP 层拒绝)或 JSON-RPC 自定义错误403 或自定义错误同 HTTP;请求凭据放 Authorization 头,不进 JSON-RPC 参数
gRPCUNAUTHENTICATEDPERMISSION_DENIEDwww-authenticate 元数据;凭据放 gRPC metadata

实现要点:

  1. 凭据在传输层携带(HTTP 头或 gRPC metadata),不要放进消息正文或任务参数;
  2. 401 响应必须带结构正确的挑战,告诉客户端该用什么方案——它对应卡片里的某个 securitySchemes 条目;
  3. 验证令牌时同时校验 issuer、audience、有效期;任何一项不过都按未认证拒绝,并且不执行智能体工作;
  4. 挑战失败的日志记录方案名与失败原因,不记录令牌本体。

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。实现清单:

  1. 用 Task 跟踪这次操作,状态转换必须真实发生在任务状态机里;
  2. 在 TaskStatus 的消息中说明需要什么授权,除非细节已带外协商;
  3. 保持响应流不断开,凭据带外到达后立即继续处理;
  4. 继续接收发给该任务的消息,客户端可能要协商、更正或拒绝;
  5. 凭据通过带外安全通道接收;沿智能体链传递时只绑定发起方可用,敏感凭据对链上其他方加密;
  6. 状态转换本身不构成授权——进入 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),不记凭据本体;授权拒绝与认证失败分开计数;
  • 告警:认证失败速率、同一身份跨租户探测模式、扩展卡片的异常请求量;
  • 轮换:按"发布新验证密钥(双读)→ 切换签发 → 吊销旧密钥"三步走;推送通知配置里的令牌遵循同样节奏,每份配置一个单一用途令牌(见推送通知安全 )。

相关页面

版本与验证说明

对齐 A2A v1.0.1。探针脚本依赖 curl;role_user、messageId、tasks/get 等名称以 proto 与数据结构 为准。本文的探针在示例端点上验证过行为分类(401/404/挑战头),没有验证任何具体身份提供方的令牌签发。

来源:A2A 认证、授权与安全注意事项 、A2A 错误码 。