用令牌交换实现 Agent 用户委托
文章介绍连接 PingFederate 与 Entra 的令牌代理,以 RFC 8693 令牌交换承接 Agent 的用户委托。设计区分目标资源所需令牌,并集中处理 Entra 权限范围与委托链。
AI 深度解读
AI agent 要访问邮件、日历和业务 API,难点不只是拿到 token:它代表谁、能做什么、出了问题怎么追责,都得一起回答。直接借用用户权限容易放大授权,统一用服务账号又容易丢失发起人的身份。
作者用 ping-entra-broker 在 PingFederate 和 Microsoft Entra ID 之间加了一层令牌代理,通过 RFC 8693 统一接收授权交换请求。值得借鉴的是它把身份、资源和权限收进同一套检查流程;但这篇只讲入口设计,还不足以证明整条授权链已经安全闭环。
一次 API 调用,要同时说明用户和 agent 是谁
原文的用户通过 PingFederate(PF)登录,PF 再通过 OIDC 与 Entra 联邦对接。agent 调用下游时,需要解决两类凭证问题:PF 保护的资源需要接受 PF token,并保留委托链;Graph 等资源需要 audience 和 scope 正确的 Entra token。
audience 表示 token 发给哪个服务,scope 表示允许执行哪些操作。仅仅确认「用户登录过」,无法回答「这个 agent 能不能替他读邮件」。
RFC 8693 的 token exchange 给请求划出了三个位置:subject token 表示借用谁的访问身份,actor token 表示谁在执行,resource 表示目标资源。其中 resource 参数采用 RFC 8707 的定义。
下面的请求把三者放进同一个交换入口:
POST /broker/oauth/token
Content-Type: application/x-www-form-urlencoded
grant_type=urn:ietf:params:oauth:grant-type:token-exchange
&subject_token=<the user's PF access token>
&subject_token_type=urn:ietf:params:oauth:token-type:access_token
&actor_token=<the agent's signed client assertion>
&actor_token_type=urn:ietf:params:oauth:token-type:jwt
&client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer
&client_assertion=<the same signed assertion>
&resource=https://graph.microsoft.com
&scope=User.Read
要看的是 actor_token 和 client_assertion:这里复用了同一份签名断言,分别承担执行者声明和客户端认证的角色。它们没有代替用户的 subject_token。
broker 会检查断言的 audience,避免截获的断言被拿到其他服务兑换;还会存储每份断言的 jti,拒绝重复使用。作者明确说,他们发现重放检查能阻止同一份断言被兑换两次。不过,原文没有提供测试过程或覆盖范围。
另一个边界是,agent 不持有用户的 Entra refresh token,也不自行拼接 Entra scope 字符串。这些细节集中交给 broker,减少每个 agent 各写一套刷新和交换逻辑的机会。
资源白名单,比 agent 自己声明权限更可信
agent 提交资源 URI,并不代表 broker 就会相信它。真正决定放行的是一份带版本的 YAML resource map,它记录资源、租户、Entra audience、scope 和交付方式。
下面的配置同时展示了资源上限和 agent 上限:
version: 1
resources:
- resource: https://api.contoso.com/orders
type: api
tenant: 11111111-1111-1111-1111-111111111111
entra_audience: api://orders-api
scopes: [orders.read, orders.write]
delivery: { default: proxy, allow_token_mode: false }
max_hops: 2
method_scopes: { GET: orders.read, PATCH: orders.write, DELETE: orders.write }
rebac:
required: true
object_from: path
object_type: order
relations: { GET: viewer, PATCH: editor, DELETE: owner }
- resource: https://graph.microsoft.com
type: api
tenant: home
entra_audience: https://graph.microsoft.com
scopes: [User.Read, Mail.Read]
delivery: { default: token, allow_token_mode: true }
max_hops: 1
agents:
- pf_client_id: report-writer
entra_agent_identity: { home: "00000000-0000-0000-0000-000000000002" }
allow:
https://api.contoso.com/orders: [orders.read]
https://graph.microsoft.com: [User.Read]
allow_token_mode: true
max_hops: 2
要看 agents.allow:orders 资源支持 orders.write,不代表 report-writer 就有写权限;Graph 支持 Mail.Read,这个 agent 也仍然只能申请 User.Read。
检查顺序很清楚:资源是否登记、agent 是否获准访问、交付方式是否允许。未知资源直接拒绝,避免一个拼错的 URI 把请求导向意外的 audience。
原文一处说权限取交集,紧接着又强调超额申请会被拒绝。结合示例,准确理解应是:白名单决定权限上限,申请越界时直接报错,不会静默缩减后继续执行。
例如申请 orders.write 会收到符合 RFC 6749 形式的 invalid_scope,拒绝决定也会写入审计日志。
max_hops 则约束 agent 之间的再次委托。样例里 orders 是 2,Graph 是 1,agent 自身是 2。整条链受沿途最小限制约束,每跳也不能扩大上一跳的权限,避免多个 agent 接力把授权越传越宽。
token 交出去以后,控制能力会发生变化
检查通过后,broker 有两种交付方式。
token mode 直接返回 Entra access token,由 agent 调用下游。Graph 示例返回 token_type: Bearer、expires_in: 3599、scope: User.Read 和 delivery: token,issued_token_type 为 urn:ietf:params:oauth:token-type:access_token。其中 3599 是示例值,不能据此推断所有 token 的固定有效期。
这种方式调用直接,但凭证一旦交出,后续请求就不必经过 broker。原文说 bearer token 可以在到期前被使用,不能把它理解成能访问任意服务:前文仍然要求它具有匹配的 audience 和 scope。
proxy mode 则把 Entra token 留在 broker,agent 拿到代理 URL 和代理专用 token,再由 broker 调用业务 API。示例入口是 https://broker.example.com/broker/proxy/orders/orders/1001,使用 curl -sS 和 authorization: Bearer $PROXY_TOKEN 请求;$PROXY_TOKEN 不是 Entra token。
orders 默认采用这种方式,因为每次调用都能经过统一的日志、限流和关系访问控制(ReBAC)检查。大白话解释:有「读订单」权限还不够,还得确认你与这张订单存在允许读取的关系。配置从路径提取对象,把 GET、PATCH、DELETE 分别关联到 viewer、editor、owner。
演示控制台还会生成每一步的 curl 视图,用 $USER_TOKEN 等名称替换真实凭证,便于查看调用结构。
我的判断
我认同它把权限判断从 agent 的临场决策里移出来。相较于每个 agent 自己维护 scope、audience、刷新和重试逻辑,集中入口更容易统一检查;相较于只用服务账号,也更有条件保留用户与执行者的对应关系。
但这是架构经验,作者没有提供维护成本、延迟或故障率的对照数据。集中之后,broker 自身的可用性、refresh token 存储,以及策略更新是否可靠,都会影响整条链。
还有一处不能跳过:配置声明了 ReBAC 规则,不等于已经证明对象级鉴权有效。原文没展开关系数据来源、路径解析和实际校验流程,也把 refresh token 如何换成 agent token、为何需要 agent identity 留给了下一篇。
采用 RFC 8693 能统一交换请求的表达方式,安全边界仍要靠具体实现落实。
能拿来做什么
- 给 Node agent 接入企业 API 时,可以先建立资源与 agent 的授权表,把资源支持的权限和单个 agent 获准的权限分别列清楚,并明确越界申请的错误行为。
- 为 token 交换入口检查 subject、actor、resource 是否齐全,同时检查签名断言的 audience 和
jti防重放,不能只停在验签成功。 - 对需要逐次审计、限流或对象关系校验的业务 API,可以评估 proxy mode;允许直接返回 token 的资源则应单独登记,并接受后续调用绕过 broker 的控制边界。
- 调试 agent 授权时,可以参考原文的 curl 展示方式,用变量名代替真实 token,让开发者看清请求结构,同时保留拒绝决策的审计信息。