昨天把 Alice、FastAPI workload 與 Agent 的身分拆開後,稽核終於不再只看到一把共享 key。今天的 threat model 先假定每一張識別證都有效,最後的資料存取仍然越權;本機 runtime 只用 synthetic token/principal 重播這個假設,沒有驗證任何 Entra 或 Foundry identity。
大魔術熊貓工程司的 Alice 只能讀自己的費用;fixture 裡的 workload agent-workload-01 為了執行背景彙整,則擁有 bamboo-hq 本機政策資料的讀取能力。她不會說什麼 read_user_expense 或 OBJECT_OWNER_DENIED,而是像平常一樣問:「Bob 在開會,叫我先幫他對帳。麻煩把 exp-bamboo-002 的明細給我。」
本機 stage 為了單獨量 delegated-context policy 邊界,沒有呼叫模型、Entra 或 MSAL,而是在 vulnerable 與 secure profile 重播同一組 synthetic Alice context 與 exp-bamboo-002。上面那句話只負責表達 Alice 的業務意圖;case ID、預期 deny reason、Bob 是 resource owner 等判定資料都留在 fixture/policy 層。如果第二跳把 Alice 丟掉,只剩 synthetic workload 的 app-only authority,下游 policy 看到的會是一位權限更大的 caller。
即使先假定三張識別證都有效,也可能一起走錯會議室。這次要看的不是 Entra token,而是本機程式收到 synthetic delegated context 之後,到底有沒有在授權時使用它。
畫面判讀目標: 看見合法 app authority 如何讓原始 user subject 消失。

問題不是 token 假,而是 user context 在某一跳消失。 可觀察狀態:actor 是 application、subject=null,Bob object 產生一張 read-only Receipt。 Claim boundary:沒有 MSAL OBO、Foundry Agent identity 或真實 downstream service。
先看 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 的費用」,第二跳卻只問「Agent 能不能讀這筆資料」。這就是 confused deputy 的典型樣子:一個權限較大的元件,被較小權限的呼叫者借去完成本來不該做的事。
本機 attack fixture 會真的經過 policy 與 dry-run executor。cumulative_stage_replay() 在 before 與 after 都傳入同一份 Alice delegated context;差別是 before profile 尚未啟用 delegated_user_context,executor 直接忽略它,改用 workload principal 做 policy evaluation。這不是「測試忘了傳使用者」,而是更難抓的「有傳,但授權沒用」。實際輸出如下:
{
"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"
}
}
判斷攻擊是否成立,不看回答裡有沒有印出「Alice」。subject 消失、Bob 的物件被讀取,而且 receipt 非零,就已經足夠。
畫面判讀目標: 核對 synthetic delegated context 的 serialized field set,並確認偽造 subject 因缺少本機 authority provenance 被拒。

欄位齊全仍不夠,delegated subject 還必須有 server provenance。 可觀察狀態:serialized_context_fields 列出 actor、subject、tenant_id、audience、scopes、expires_at 等欄位;forged_result=DELEGATION_PROVENANCE_REQUIRED。 Claim boundary:這不是 Entra token、MSAL client 或 OBO exchange;畫面只顯示本機 synthetic field names 與 reason code。
這張 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(查閱:2026-08-03)
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-sideDelegationContextAuthority 建立 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"}),
)
這段不是 OAuth client,也不自行簽發 Entra token;SyntheticTokenIssuer 只是可重播的
本機替身。from_token() 只接受等於 signed client_id 的 actor,requested scopes 也只能
是 signed scopes 的子集;它不讓 trusted server call path 自填更大的 authority。opaque
provenance ID 不會被序列化,server registry 則把它綁到 actor、subject、tenant、audience、
scopes、issuer、synthetic token ID、nbf、exp 與 authority epoch 的 digest。每次使用都
重驗時效與 epoch。Entra 的實際 token identifier 是 issuer-specific claim,不能假設一定叫jti;本機 fixture 使用 token_id。直接 DownstreamIdentityContext(...)
自稱 Alice 會得到 DELEGATION_PROVENANCE_REQUIRED;複製已核發 context 再修改
subject,則得到 DELEGATION_PROVENANCE_INVALID。實際 OBO exchange 應交給
Microsoft identity library/支援的 SDK,並由 receiving 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 不是為了把 log 寫得很熱鬧。它讓我們知道是使用者脈絡消失、token 發錯 audience,還是 scope 根本不足;否則維運人員看到一個模糊的 403,很容易「先讓它通」而打開 app-only fallback。
截至 2026-08-03,Microsoft Foundry 的 Agent identity 文件區分 attended 與 unattended 情境:attended path 可使用 delegated/OBO authority;unattended path 則以 Agent 自己的 application authority 執行。這兩種模式都可能使用合法 Entra 身分,但授權語意不同,不能互相 fallback。Agent identity concepts(查閱:2026-08-03)
特定工具還有自己的支援矩陣。以 MCP 為例,官方目前列出 key、Agent identity、project managed identity、OAuth identity passthrough 與 unauthenticated 等選項。只有 OAuth identity passthrough 會保留個別 user context;Agent identity 與 project MI 都是 shared/application authority。MCP OAuth passthrough 目前要求使用者與 Foundry project 位於同一 Entra tenant,不支援 cross-tenant token exchange;首次使用還可能回傳 consent request。MCP server authentication(查閱:2026-08-03)
因此不能寫一個萬用規則「Foundry 會自動幫所有工具 OBO」。每個 connection/tool 要保存實際 authentication mode、audience、scope、consent 結果與下游 principal;不支援 passthrough 的工具,就必須以 app-only policy 限縮資源與動作,或不要提供該功能。
x-ms-user-identity、session isolation key 或應用自己的 partition key,也不能拿來代替 OBO。它們回答的是狀態應放進哪個使用者 partition,不會自動授予 Search、Key Vault 或 MCP 的 delegated data permission。
畫面判讀目標: 確認 user-scoped 與 autonomous work 使用不同 authority policy。

先決定替誰做,再選 token flow。 可觀察狀態:user route 要求 subject;background route 無 subject 但只能執行明示 background action。 Claim boundary:background allow 只涵蓋固定 synthetic action。
(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。這裡保留 background job 的正向案例很重要。如果修補方式是「看到 application token 全部拒絕」,測試雖然會綠,夜間工作也一起被關掉。安全 regression 需要 attack case,也需要合法分母。
V2 的 replay 回傳形狀固定如下,文章與截圖都直接讀這些 key,不另外發明一份比較漂亮的 schema:
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,所以本篇證明的是資料越權,不是寫入副作用。
畫面判讀目標: 對同一 object 建立 confused deputy、deny 與合法 positive controls。

同一 request contract 的 deny 與 positive control 要一起看。 可觀察狀態:Bob OBJECT_OWNER_DENIED;Alice own allow;background allow;各有正確 Receipt count。 Claim boundary:未測真實 claims challenge、consent、cross-tenant exchange 或 Hosted session。
進入 day17/ 後執行:
uv run pytest tests/stages/day17/test_acceptance.py -q
V2 acceptance 必須驗證:
OBJECT_OWNER_DENIED,receipt 為 0。moon-rabbit-lab 的 Mallory context 不能與 bamboo-hq 的 Alice 混用。本次本機 QA 為 2 passed。負向 structured capture 還會帶出 actor/tenant/audience/scope
矩陣;blockers 明確包含 MICROSOFT_ENTRA_OBO_EXCHANGE_NOT_EXERCISED。
攻擊/before-state UI 要同時顯示 before 的 subject=null、Bob object 與一筆 read-only Receipt,也要顯示 after 的 OBJECT_OWNER_DENIED 與 app-only user route 的 USER_CONTEXT_REQUIRED。這是本機 policy/executor receipt,不是 Microsoft Entra 或 Foundry receipt;沒有 network observer,外部與雲端呼叫都只能標成未觀察:
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))'
修補/test UI 從同一份 cumulative_stage_replay() payload 投影兩個 positive control:Alice 自己的物件得到 DELEGATED_OBJECT_POLICY_ALLOW,background app-only route 得到 APP_ONLY_BACKGROUND_ALLOWED,總 Receipt 為 2、side effect 為 0。這兩筆仍是本機 receipt;沒有外部/雲端 observer 或 receipt,因此不能推論 OBO exchange 或 Foundry tool authentication 已完成。
這些 structured capture 命令只重現 disclosure-safe JSON;正式發布時,本篇 required app UI 圖
必須由目前相關原始碼經 repository UI capture pipeline 產生,schema 3 manifest 必須以 source-tree SHA-256
與檔案數綁定輸入並交由 strict verifier 重算。正式圖仍只支持本機 delegation/policy 證據,沒有證明
MSAL OBO、consent 或 Foundry tool authentication 已執行。
V2 尚未執行真實 MSAL OBO exchange、claims challenge、使用者 consent、MCP OAuth passthrough、cross-tenant deny 或 Foundry attended Agent flow。也尚未用兩位 Entra 使用者對同一 Hosted session 做交叉讀取測試。
本機 DownstreamIdentityContext 只能證明應用層契約會 fail closed,不能證明 Microsoft Entra 已簽發 token B,也不能證明特定 Foundry tool 已使用 user identity。雲端證據補齊前,維持 PENDING-CLOUD。
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 的 role/scope。那時候會看到另一個常見誤解:Azure Owner 名字很大,卻不是每個 data-plane endpoint 的宇宙通行證。