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。