OBO 代表用户访问:集成指南

OBO 让 API A 代表当前用户调用 API B,并在 Token 中记录 A 是代理调用的客户端。两个 Token 都由 Signet 签发,不需要 Microsoft Entra 或 Entra Token。本功能是单跳标准 OBO,不包含 Agent OBO。

使用场景

场景 适用流程
前端的 BFF 按照当前用户的权限调用订单 API OBO:BFF 是 A,订单 API 是 B
客服助手或 MCP Gateway 代表已登录用户调用工具 A 为预先注册的机密客户端,且用户已同意时,可用 OBO
定时任务以应用程序自己的身份执行,没有用户 使用客户端凭证流程,不是 OBO
前端需要让用户登录、获取第一个 Token 先使用授权码 + PKCE 或设备流程
需要 A → B → C 多跳委派,或 Entra Agent 身份/Blueprint 此版本不支持

不要直接把 audience 为 A 的原始 Token 传给 B。OBO 会创建专门给 B、只包含明确允许范围的 Token;B 仍须判断用户是否有权读取指定订单。

sequenceDiagram participant F as Frontend F participant A as API A participant S as Signet participant B as API B Note over F,S: User consents to F → A and A → B F->>A: User access token (aud=A) A->>S: OBO: A credentials + user token + resource B + scope S->>S: Check policy, source token, both consents S-->>A: User access token (aud=B, actor=A) A->>B: Bearer OBO token B->>B: Validate token and user permissions

1. 管理员配置

示例假设 Signet 位于 https://signet.example.com。请替换客户端 ID、回调地址、API 资源 URI 与密钥。

角色 注册配置与职责
前端 F 授权码 + PKCE(或设备流程);注册范围 orders.delegate.read;允许资源 https://api-a.example.com
API A 已启用、非 CIMD 的 confidential 机密客户端;密钥只保存在后端;注册范围 orders.read;允许资源 https://api-b.example.com;为下游同意流程启用授权码流程并注册回调地址
API B 验证 https://api-b.example.com audience 的资源服务器;若调用 introspection,另备 B 自己的机密客户端凭证
Signet 管理员 启用 OBO 并指定委派策略;应用程序不能自行声明拥有来源 audience

没有每个客户端各自的“OBO 复选框”:委派权限由管理员策略授予。A 执行 OBO 不需要启用 Client Credentials 流程。

将以下 JSON 保存为 Signet 服务器上的 /etc/signet/obo-policies.json;使用容器时可只读挂载:

[
  {
    "id": "orders-a-to-b",
    "actor_client_id": "API_A_CLIENT_ID",
    "inbound_audience": "https://api-a.example.com",
    "target_resource": "https://api-b.example.com",
    "scope_mapping": {
      "orders.read": ["orders.delegate.read"]
    }
  }
]

scope_mapping 的方向是“输出范围 → 必须全部具备的输入范围”。来源 Token 有 orders.delegate.read,才可请求 orders.read;同时 A 的注册范围与用户对下游的同意也都必须允许。不是把来源所有 scope 原样复制。

配置 Signet:

OBO_ENABLED=true
OBO_TOKEN_EXPIRATION=5m
OBO_POLICIES_FILE=/etc/signet/obo-policies.json

OBO 默认关闭;没有策略就拒绝所有交换。策略在启动时加载,更新后需让所有副本使用相同配置与策略并重新启动。有效期必须大于零且不超过五分钟。

资源 URI 精确匹配,结尾斜杠也有差异。请使用 HTTPS;只有 localhost、127.0.0.1、::1 开发主机允许 HTTP。来源 A 与目标 B 必须不同。A 的允许资源列表本身不代表 A 拥有来源 audience。

2. 获取用户的来源 Token(F → A)

将用户浏览器重定向到下列授权请求。换行仅供阅读,实际请组成一个正确编码的 URL:

GET /oauth/authorize
  ?response_type=code
  &client_id=FRONTEND_CLIENT_ID
  &redirect_uri=https%3A%2F%2Ffrontend.example.com%2Fcallback
  &scope=orders.delegate.read
  &resource=https%3A%2F%2Fapi-a.example.com
  &state=F_RANDOM_STATE
  &code_challenge=F_S256_CHALLENGE
  &code_challenge_method=S256

按照授权码 + PKCE生成新的 verifier/challenge、处理回调、验证 state 与精确的 iss,再到 /oauth/token 交换授权码。获取的 access token 在后续示例称为 USER_ACCESS_TOKEN_A。F 调用 A 时放在 Authorization: Bearer ...。

Token 必须只有一个 audience:https://api-a.example.com,且具有已记录的用户同意。其 client_id 是 F,不是 A。不能拿 ID Token、Refresh Token、API Key、Client Credentials Token、外部 Token,或缺少 audience/同意记录的旧 Token 代替。

3. 另获取下游用户同意(A → B)

同一位用户还必须同意让 A 访问 B。由 A 启动另一个浏览器授权流程:

GET /oauth/authorize
  ?response_type=code
  &client_id=API_A_CLIENT_ID
  &redirect_uri=https%3A%2F%2Fapi-a.example.com%2Fcallback
  &scope=orders.read
  &resource=https%3A%2F%2Fapi-b.example.com
  &state=A_RANDOM_STATE
  &code_challenge=A_S256_CHALLENGE
  &code_challenge_method=S256

A 处理自己的注册回调,使用 A 的凭证及此流程的 PKCE verifier 完成普通授权码流程。验证 state 与 iss,并将流程绑定原始用户会话;若登录成另一位用户,必须拒绝。不可重用 F 的授权码或 verifier。

这会创建“用户/A/资源 B”的同意记录。这次配置流程获取的 Access Token 不是 OBO assertion;assertion 仍是步骤 2 中 F 获取、audience 为 A 的 Token。

两份同意都必须有效,且允许对应范围。/oauth/token 无法显示同意页面;缺少或撤销同意时,应先安排交互授权,再重试交换,不能无限重试 invalid_grant。对多个资源一起同意,不等同本示例单独的 A → B 资源集合。

4. 在 A 后端交换 Token

在后端安全配置/运行环境中提供 SIGNET_URL=https://signet.example.com、A 的客户端 ID/密钥,以及 USER_ACCESS_TOKEN_A。绝对不要把 A 的密钥放到浏览器,也不要记录原始 Token。

# Run on API A's backend; populate these variables securely.
curl --request POST "$SIGNET_URL/oauth/token" \
  --user "$API_A_CLIENT_ID:$API_A_CLIENT_SECRET" \
  --header 'Content-Type: application/x-www-form-urlencoded' \
  --data-urlencode 'grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer' \
  --data-urlencode 'requested_token_use=on_behalf_of' \
  --data-urlencode "assertion=$USER_ACCESS_TOKEN_A" \
  --data-urlencode 'resource=https://api-b.example.com' \
  --data-urlencode 'scope=orders.read'

五个表单参数均必填;表单内只能有一个 resource,/oauth/token 端点 URL 不可附 query 参数。这与表单 resource URI 本身的 query 不同:建议使用不含 query 的资源标识符;若确有需要,完整 URI(包含 query 或结尾的 ?)必须与策略、允许列表及用户同意精确一致。可改用表单 client_id/client_secret,但不能与 Basic 认证混用。重复参数或不支持的参数(例如 extra_claims、actor_token、client_assertion)会被拒绝。此端点不接受 JSON 请求。

响应示例:

{
  "access_token": "eyJ...",
  "token_type": "Bearer",
  "expires_in": 300,
  "scope": "orders.read"
}

expires_in 可能小于 300,因为有效期同时受来源 Token 剩余时间、A 的 Token 配置与 OBO_TOKEN_EXPIRATION 限制。不会返回 Refresh Token 或 ID Token。

5. 调用与保护 B

A 将响应的 access_token 取出作为 OBO_ACCESS_TOKEN_B,再调用 B:

curl https://api-b.example.com/orders \
  --header "Authorization: Bearer $OBO_ACCESS_TOKEN_B"

B 必须验证签名、预期的 Signet issuer、有效期、type=access、自己的 aud、必要 scope,以及用户/代理客户端 claims:

Claim 含义
sub/user_id 原始用户
client_id A 的客户端 ID
act.sub client:<A 的客户端 ID>
aud B 的资源 URI
scope 批准的下游范围,例如 orders.read

请参考 JWT 验证,优先使用 RS256/ES256 与公开 JWKS,不要把 Signet 的 HS256 签名密钥分发给独立 API。B 还必须执行业务授权,例如确认这位用户能否读取这笔订单。

若需要立即撤销,B 应在每次受保护操作时,以自己的机密客户端凭证额外调用 /oauth/introspect,且不缓存有效响应。请参考 Token 与撤销。当 INTROSPECTION_REQUIRE_OWNERSHIP=true 时,不同客户端 B 查询 A 的 Token 只会获取 active,因此仍需本地 JWT 验证 audience 与 claims;active=true 本身不代表有权执行操作。查询失败、超时或被限流时应拒绝操作。

在线验证会检查来源 Token、用户、两个客户端、两份同意与当前策略。只有离线 JWT 验证,无法在到期前感知撤销。

更新、缓存与错误排查

来源 Token 在有效期间可重复交换。B Token 到期时,A 使用仍有效的来源 Token 再做 OBO;来源到期则由 F 通过原始流程更新。OBO 结果不能再交换:不支持多跳委派。

若 A 缓存 B Token,必须按用户、来源 Token 身份、代理客户端、目标资源与请求范围隔离,并在返回有效期前失效。不可跨用户共享 Token,也不可将原始 assertion/密钥存入日志或缓存键。A 的 Token 缓存不能取代 B 的即时撤销检查。

错误 检查项目
unsupported_grant_type 所有 Signet 副本是否启用 OBO
invalid_request 表单编码、五个必填字段、重复/未知字段、URL query
invalid_client(401) A 是否启用、密钥是否正确、是否只使用一种认证方式
unauthorized_client A 是否为非 CIMD 机密客户端,是否有匹配的管理员策略
invalid_grant 来源 Token 是否有效且单一 audience、用户/客户端是否启用、两份同意是否有效
invalid_scope 输出对输入 scope 映射、A 注册范围、下游用户同意
invalid_target B URI 是否精确一致、URL 格式、A 的允许资源列表

授权错误不能原样反复重试,应先补同意或修正配置。暂时性失败可有限次退避重试,但不能改用应用程序身份绕过用户被拒绝的权限。

功能边界与操作安全

  • 部署新版前必须先升级数据库,即使 OBO_ENABLED=false:普通 Token 写入也需要新字段。默认由启动 migration 处理;使用 DB_AUTO_MIGRATE=false 时,migration 负责人须先新增允许 NULL 的 text 字段 access_tokens.source_token_id、access_tokens.delegation_policy_id,以及 source_token_id 上的索引 idx_access_tokens_source_token_id。先备份、演练,重试前检查现有字段与索引。详见仓库的 SQL 升级与验证步骤。先验证普通 Token 签发,再启用 OBO;旧副本仍存在时不要启用。回退版本时保留新增 schema。
  • 请求使用 jwt-bearer + requested_token_use=on_behalf_of,并要求 resource;不宣称完整 MSAL 兼容,也不是 RFC 8693 的 token-exchange 请求格式。
  • 不包含 Agent OBO、Agent Blueprint、T1、权限继承、.default、Entra Token 或 claims challenge。
  • act、may_act 在所有 grant 中都是签发者保留 claims,即使 OBO 关闭也一样。现有同名自定义 claims 必须改名。
  • 所有副本关闭 OBO_ENABLED 后停止签发并拒绝在线验证;离线端仍可能接受未到期 Token。降级到不支持 OBO 的版本前,撤销委派 Token 并等待其最长有效期(五分钟),保留新增的数据库字段。

相关文档

不重启管理策略

OBO_POLICY_SOURCE=file 为默认值,保留原有文件流程。
OBO_POLICY_SOURCE=database 仅使用权威数据库,不能同时设置策略文件。
管理员从 Client 详情页进入「API 资源与委派权限」,登记各 API 的 scopes、
audience 所有者以及明确的输出→必要来源 scope 映射。Client scopes 与允许资源仍须
单独设置;策略不是用户同意。权限修改、停用及重新启用会通过策略版本让旧 OBO
Token 在线失效;显示名称变更不撤销 Token。

完成 schema 迁移后,通过管理界面创建资源、scope 与策略。
切换来源前停止/排空所有 OBO 副本并等待五分钟,不混用 file/database 副本,
不自动退回旧文件。回滚保留增量 schema,重新启用前明确比对有效规则。

合并用户同意

在 database 模式启用 OBO_ENABLED=true、OBO_COMBINED_CONSENT_ENABLED=true 与
LOGIN_SESSION_TRACKING_ENABLED=true,管理员可为 F 与来源 A 指定下游策略及 scopes。
单一 resource 的 Authorization Code 请求会一起显示 F→A 与 A→B,批准后保存独立
授权,只发出 F 原本用于 A 的 code。A 不需要同意 callback,OBO 仍使用自己的机密
凭据。Device Flow 维持独立同意;不新增 MSAL、Entra Token 或代表全体用户同意。

服务器将十分钟、单次使用的 handle 绑定用户与 session,重新检查权限快照,并以
事务原子提交 grants/code。内容变更需重新发起;已覆盖的 grant 保持不变,新增
scopes 可能让旧 Token 失效。SkipConsent 不会自动新增下游同意。
账户页保留逐笔撤销;撤销 A→B 也会影响同一用户通过其他前端使用该 API 的授权。

数据库模式目前读取完整 registry 快照。即时 OBO 撤销需在线验证;离线 JWT 与普通
非 OBO Token 维持原有行为。完整 schema SQL、回滚及 PostgreSQL 验收命令请见
项目的 docs/ON_BEHALF_OF_FLOW.md。