昨天分開了使用者與 workload 身分。今天假設兩者都能通過認證,再看下一跳是否仍然使用 Alice 的權限。
Alice 只能讀自己的費用,卻要求讀 Bob 的 exp-bamboo-002。本機 workload agent-workload-01 另有背景查詢能力。如果授權時忽略 Alice 的 delegated context,就可能改用 workload 較大的權限放行。
修補前後都傳入同一份 Alice context,差別只在 executor 有沒有採用它。這個測試直接操作合成身分與費用資料,不會呼叫模型、MSAL 或 Entra。
先看授權只採用 application 身分時,原本的使用者去哪裡了。

actor 是 application,subject=null,Bob 的費用卻留下 1 張唯讀 Receipt。這次問題在於授權沒有採用使用者的限制。測試沒有執行 MSAL OBO、Foundry Agent identity 或真實的下游服務。
先看 vulnerable path:
agent-workload-01(application principal 的本機替身)
+ Alice delegated_context(有傳入)
→ before profile 忽略 delegated_context
→ policy 改以 workload principal 評估
→ exp-bamboo-002(owner=bob)被讀出,Receipt=1
這條本機路徑沒有任何 Entra token;可驗證的只有 synthetic fixtures:
SyntheticTokenIssuer 的本機驗證。auth_type=synthetic-application 的 Principal,不是 FastAPI 驗過的 Entra workload identity。service-reader role;程式沒有建立或驗證 Foundry Agent identity。原本要確認 Alice 能否讀 Bob 的費用,流程卻只檢查 workload 能否讀取。這是 confused deputy:服務有能力存取資源,但應該把這次使用者的限制一起帶入決策。
cumulative_stage_replay() 兩次都傳入 Alice 的 delegated context。修補前沒有啟用 delegated_user_context,executor 因此忽略使用者,改用 workload principal 做授權。這個問題比漏傳參數更容易被忽略:資料明明有帶到,授權時卻沒用。輸出如下:
{
"action": "read_user_expense",
"actor": "agent-workload-01",
"subject": null,
"resource_id": "exp-bamboo-002",
"reason_code": "APP_AUTHORITY_CONFUSED_DEPUTY",
"receipt_count": 1,
"side_effect_count": 0,
"details": {
"delegated_context_enforced": false,
"expense_owner": "bob",
"policy_reason": "POLICY_ALLOW"
}
}
觀察結果時,對照授權使用的 subject、讀取的物件與 Receipt。這次 subject 消失後,Bob 的物件確實被讀取,因此已構成資料存取越權。
再檢查 delegated context 保存了哪些欄位,以及偽造 subject 為什麼被拒絕。

serialized_context_fields 列出 actor、subject、tenant_id、audience、scopes、expires_at 等欄位;偽造資料得到 forged_result=DELEGATION_PROVENANCE_REQUIRED。光填齊欄位還不夠,subject 必須有 server 核發的來源依據。畫面只列出本機合成資料的欄位名稱與拒絕原因,沒有 Entra token、MSAL client 或 OBO 交換。
這張 inspector 只讀取本機 helper 的 disclosure-safe JSON,不建立或交換 token:
PYTHONPATH=src:. uv run python -c 'import json; from magic_panda_agent.stages.day17 import delegated_context_evidence; print(json.dumps(delegated_context_evidence(), ensure_ascii=False, sort_keys=True))'
On-Behalf-Of(OBO)用來讓 middle-tier API 延續使用者的 delegated authority:
User
└─ token A:aud = Magic Panda API
└─ FastAPI 驗 signature/issuer/audience/tenant/subject
└─ FastAPI 以 confidential-client 身分提出 OBO exchange
└─ Microsoft identity platform
└─ token B:aud = downstream Expenses API
Microsoft 的 OBO 文件要求 assertion token 的 aud 必須是提出交換要求的 middle-tier application。拿一顆原本發給 Microsoft Graph 或其他 API 的 token 來兌換,middle tier 應拒絕;token A 也不應直接 relay 到下游。直接轉送會破壞 intended audience、Conditional Access claims challenge 與 token binding 的邊界。Microsoft identity platform OBO flow
OBO 使用 delegated scopes,不是把 application role 假裝成某位使用者。夜間批次工作沒有互動使用者,應走 app-only/Managed Identity/Agent identity 路徑,再由 application policy 限制;不能發明一位永遠在線的 system-user 來填欄位。
大魔術熊貓工程司把工作分成兩條入口:
| 工作 | authority mode | 下游授權鍵 | 失敗時 |
|---|---|---|---|
| Alice 查自己的費用 | delegated/OBO | actor + subject + tenant + scope + object | fail closed,不改用 Agent privilege |
| Alice 觸發需使用者權限的工具 | delegated 或該工具支援的 OAuth passthrough | 使用者 consent 與下游 scope | 回傳需要 consent/授權,不 fallback |
| 夜間費用彙整 | application/autonomous | workload identity + application capability | 走獨立 background policy |
| Agent 讀共用政策 | application/Agent identity | Agent role + tenant/corpus policy | 不產生使用者身分 |
DownstreamIdentityContext 刻意把 actor 與 subject 分開。更重要的是,它不能由 request body 直接建出一份「看起來像 Alice」的資料就取得 authority。當日 lab 先驗證一顆受信 synthetic delegated token,再由 server-side DelegationContextAuthority 建立 opaque provenance;actor 是這一跳真正送 request 的 service/Agent,subject 才是它所代表的使用者:
from magic_panda_agent.auth import (
DelegationContextAuthority,
SyntheticTokenIssuer,
)
from magic_panda_agent.models import Principal
issuer = SyntheticTokenIssuer(key=b"SYNTH_MAGIC_PANDA_LOCAL_SIGNING_KEY_32B")
authority = DelegationContextAuthority(issuer)
delegated_token = issuer.issue(
Principal(
subject="alice",
tenant_id="bamboo-hq",
roles=frozenset({"employee"}),
auth_type="synthetic-delegated",
),
audience="api://expense-data",
client_id="agent-workload-01",
scopes=frozenset({"Expenses.Read"}),
)
context = authority.from_token(
delegated_token,
actor="agent-workload-01",
audience="api://expense-data",
scopes=frozenset({"Expenses.Read"}),
)
context.require_user_context(
authority=authority,
expected_actor="agent-workload-01",
expected_tenant_id="bamboo-hq",
expected_audience="api://expense-data",
required_scopes=frozenset({"Expenses.Read"}),
)
這段使用 SyntheticTokenIssuer 作為本機替身。from_token() 要求 actor 與已簽署的 client_id 相同,requested scopes 也只能是 signed scopes 的子集,呼叫端不能自行加大權限。
Server registry 將不對外序列化的 provenance ID,綁到 actor、subject、tenant、audience、scopes、issuer、synthetic token ID、nbf、exp 與 authority epoch 的 digest,每次使用再檢查時效與 epoch。直接建立一份自稱 Alice 的 context,會得到 DELEGATION_PROVENANCE_REQUIRED;複製已核發資料再改 subject,則得到 DELEGATION_PROVENANCE_INVALID。
本機 token 使用 token_id,真正 Entra token 的識別 claim 則依 issuer 而定,不能固定假設為 jti。實際 OBO 交換交給 Microsoft identity library 或支援的 SDK,接收 API 再驗證取得的 token。
如果 mode="application"、subject=None,user-scoped route 必須回 USER_CONTEXT_REQUIRED。如果 actor、tenant、audience 或 scope 只有一項不符,則分別回:
DOWNSTREAM_ACTOR_DENIED
DOWNSTREAM_TENANT_DENIED
DOWNSTREAM_AUDIENCE_DENIED
DOWNSTREAM_SCOPE_DENIED
不同 reason code 對應不同的補救位置:缺少使用者脈絡、audience 錯誤與 scope 不足,不能都靠提高 workload 權限處理。保留原因,才能讓失敗停在正確的授權邊界。
在 Alice 查自己費用的路徑,先問下游到底收到誰的 token。若 Search 或 MCP 只看見 Agent 的 application identity,還要由工程司明確限制這條 app-only 查詢能讀哪些物件;若要替 Alice 查個人資料,則需走支援 delegated authority 的路徑,並在下游看到 Alice 的 subject。session_id 相同或 Agent 回答正確,都不能代替這項檢查。
Microsoft Foundry 的 Agent identity 文件區分 attended 與 unattended 情境:attended path 可使用 delegated/OBO authority;unattended path 則以 Agent 自己的 application authority 執行。這兩種模式都可能使用合法 Entra 身分,但授權語意不同,不能互相 fallback。Agent identity concepts
特定工具還有自己的支援矩陣。以 MCP 為例,官方目前列出 key、Agent identity、project managed identity、OAuth identity passthrough 與 unauthenticated 等選項。只有 OAuth identity passthrough 會保留個別 user context;Agent identity 與 project MI 代表的都是應用程式自己的權限,不帶 Alice 的身分。MCP OAuth passthrough 目前要求使用者與 Foundry project 位於同一 Entra tenant,不支援 cross-tenant token exchange;首次使用還可能回傳 consent request。MCP server authentication
每個 connection 或 tool 都要記錄實際 authentication mode、audience、scope、consent 結果與下游 principal。沒有 passthrough 支援的工具,就得用明確的 app-only policy 限縮操作,或不提供該功能,不能默認會自動延續 user authority。
x-ms-user-identity、session isolation key 或應用自己的 partition key,也不能拿來代替 OBO。它們回答的是狀態應放進哪個使用者 partition,不會自動授予 Search、Key Vault 或 MCP 的 delegated data permission。
接著比較替使用者操作與自主背景工作,各自使用哪一套授權條件。

user route 要求 subject;background route 可以沒有 subject,但只能執行明確允許的背景動作。這裡的 background allow 只涵蓋固定的合成案例,還不能延伸到所有背景工作。
(tid, oid)。user_scoped 或 autonomous;不由 prompt 決定。user_scoped 工作要求 delegated context,且 actor、subject、tenant、audience、scope 全部符合。OBJECT_OWNER_DENIED。autonomous 工作走獨立 background policy,沒有 subject,也不能呼叫 user-scoped action。背景工作本來就沒有互動使用者,所以還需要 app-only 的正常案例。測試應確認它只能走 background action,同時確認 Alice 的 user-scoped 請求不能轉用這條權限;直接拒絕所有 application token 會破壞原有工作。
Replay 的回傳欄位如下,可以直接對照程式與畫面:
from magic_panda_agent.stages.day17 import cumulative_stage_replay
evidence = cumulative_stage_replay()
assert evidence["declared_delta"] == (("delegated_user_context", True),)
assert evidence["confused_deputy"]["reason_code"] == (
"APP_AUTHORITY_CONFUSED_DEPUTY"
)
assert evidence["object_denied"]["reason_code"] == "OBJECT_OWNER_DENIED"
assert evidence["own_object_allowed"]["reason_code"] == (
"DELEGATED_OBJECT_POLICY_ALLOW"
)
assert evidence["app_only_user_denied"]["reason_code"] == (
"USER_CONTEXT_REQUIRED"
)
assert evidence["background_allowed"]["reason_code"] == (
"APP_ONLY_BACKGROUND_ALLOWED"
)
assert evidence["before_tool_receipts"] == 1
assert evidence["after_tool_receipts"] == 2
assert evidence["day16_identity_preserved"] is True
assert evidence["future_controls_active"] == []
before_tool_receipts=1 是 Bob 物件真的被 read-only tool 讀取;after 的兩筆則來自 Alice 自己的物件與合法 background policy query。兩邊的 side_effect_count 都是 0,所以本篇證明的是資料越權,不是寫入副作用。
先重送讀取 Bob 物件的請求,再用 Alice 自己的物件與合法背景工作作對照。

讀取 Bob 物件得到 OBJECT_OWNER_DENIED;Alice 讀自己的物件與合法背景工作則能通過,各自保留 Receipt 次數。這裡沒有測真實的 claims challenge、consent、跨租戶 token 交換或 Hosted session。
進入 day17/ 後執行:
uv run pytest tests/stages/day17/test_acceptance.py -q
這組驗收要確認:
OBJECT_OWNER_DENIED,receipt 為 0。moon-rabbit-lab 的 Mallory context 不能與 bamboo-hq 的 Alice 混用。本機測試記錄為 2 passed。負向結果另列 actor、tenant、audience 與 scope 的對照,並保留 MICROSOFT_ENTRA_OBO_EXCHANGE_NOT_EXERCISED,表示這次沒有完成真實 OBO 交換。
先看修補前的 subject=null、Bob 的物件與 1 張唯讀 Receipt,再看修補後的 OBJECT_OWNER_DENIED,以及 app-only 走使用者路徑時的 USER_CONTEXT_REQUIRED。這些都是本機 policy/executor 的結果,沒有觀測雲端呼叫:
PYTHONPATH=src:. uv run python -c 'import json; from magic_panda_agent.stages.day17 import cumulative_stage_replay; print(json.dumps(cumulative_stage_replay(), ensure_ascii=False, sort_keys=True))'
同一份 cumulative_stage_replay() 也保留兩筆合法操作:Alice 讀自己的物件,得到 DELEGATED_OBJECT_POLICY_ALLOW;背景查詢得到 APP_ONLY_BACKGROUND_ALLOWED。合計 2 張 Receipt、0 次副作用,確認我們沒有把正常的背景工作一起封鎖。
OBO 保留 user authority,也帶來 confidential-client credential、consent、token cache、refresh 與 claims challenge。access/refresh token 若進入 prompt、trace、browser storage 或 exception,新的資料外洩面就跟著出現。使用者撤權後,既有 token 與下游 cache 也未必在同一瞬間全部消失,正式系統要測 revocation 與 bounded cache lifetime。
NIST SP 800-207A 的逐請求決策,在這裡落成每一跳綁定 actor、subject、tenant、audience、scope、resource 與 action。它沒有要求所有工作都假裝成使用者;反而應把 delegated 與 autonomous authority 清楚分開。
下一篇把這些 principal 放進 Foundry、Search 與 Key Vault 的角色和 scope,看看管理資源、呼叫 Agent 與讀取資料,各自需要什麼權限。