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 仍须判断用户是否有权读取指定订单。
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。