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。