iT邦幫忙

2026 iThome 鐵人賽

DAY 12
0
AI Engineering

當企業導入 Agent:30 天拆解治理邊界與平台選型系列 第 12 篇

Day 12|AWS Cognito 雙路徑實戰:同一個 Issuer,拆開 Human 與 M2M

  • 分享至 

  • xImage
  •  

同一個 Observability MCP 有兩種呼叫者。值班工程師從 CLI 登入後查資料,Scheduler 則在沒有人操作時定期執行。兩枚 Access Token 都由同一個 AWS Cognito User Pool 簽發,我起初也很自然地把它們放進同一套 JWT 驗證規則。

真正攤開 Claims 後,兩條路徑的差異剛好落在 Gateway 不能含糊的地方。Human Token 有登入者的 sub,也能透過 Resource Binding 帶入目標 API 的 aud。Client Credentials 取得的 M2M Token 沒有 Human,AWS Cognito 也不支援同一套 Resource Binding。若 Gateway 要求所有 Token 都有指定的 sub 與 aud,合法的 Scheduler 會被擋掉。若乾脆取消 Audience 檢查,Human 路徑又失去原本的 Resource Boundary。

要讓值班工程師和 Scheduler 使用同一個入口,平台得把 App Client、Token 條件、Secret 輪替與 Gateway Policy 一起拆開。Issuer 和 JWKS 可以共用,下游用來辨認與授權兩種呼叫者的規則則各自保留。這篇沿著兩條請求路徑,說明這些設定如何配合。

同一個 User Pool,兩個 App Client

AWS Cognito 官方 AWS Architecture Icon。

選定 AWS Cognito 之後,我們把 Human CLI 與 Scheduler 分別註冊成兩個 App Client。只用「Agent Client」當名稱,過幾個月後通常已看不出它代表登入者、Scheduler,還是某個 Runtime。兩條路的 Grant 和 Secret Lifecycle 也會跟著混在一起。

設定 Human CLI Scheduler M2M
App Client Public Confidential
Grant Authorization Code Client Credentials
Durable Secret 不應存在 必須保存與輪替
PKCE S256 不適用
Callback Exact Allowlist 不適用
Token Principal 登入的 Human App Client

AWS Cognito App Client 文件 將沒有 Client Secret 的應用視為 Public Client,有 Secret 的應用視為 Confidential Client。Client Credentials 只能使用 Confidential Client,而且不能和 Authorization Code 或 Implicit Grant 放在同一個 App Client。這不是偏好的命名方式,而是 AWS Cognito 的 App Client 規則從一開始就要求兩份設定。

Human Token 保留登入者與目標 Resource

Human CLI 使用沒有 Durable Secret 的 Public App Client,走 Authorization Code + PKCE。Callback 必須精確列入 Allowlist,Scope 也要同時出現在 Resource Server 與 App Client 設定中。以下是離線 Lab 的合約輸入:

app client: sre-console
secret:     none
grant:      authorization_code
callback:   http://127.0.0.1:8765/callback
scope:      openid platform/observability.query
resource:   https://observability.lab.example/mcp

其中 127.0.0.1 是離線 Fixture 的 Callback,不代表已完成 Cognito 註冊。AWS App Client 文件 只明列 http://localhost 作為測試用的 HTTP 例外,正式設定需依 Client 的 Redirect URI 與 Cognito 接受條件核對。Day 11 已經談過兩種 Host 不可互換的 Callback 坑。來到 AWS Cognito 後,另一個重點是 resource。AWS Resource Binding 文件 說明 Managed Login 的 Authorization Code User Flow 可以把 Resource URL 寫入 Access Token 的 aud。下游除了驗證 Token 來自哪個 User Pool,也能確認它是不是簽給自己的。

公開 Lab 用本機簽發的合成 Token 示範兩份合約,沒有連上 AWS Cognito。Human Token 保留以下 Claims:

{
  "iss": "https://cognito-idp.ap-northeast-1.amazonaws.com/ap-northeast-1_LabPool",
  "aud": "https://observability.lab.example/mcp",
  "sub": "user/sre-oncaller",
  "client_id": "sre-console",
  "scope": "platform/observability.query",
  "token_use": "access"
}

sub 指向登入者,client_id 保留入口,aud 限制目標服務。AWS Cognito Access Token 不保證 Header 一定有 typ=at+jwt,因此驗證時不能為了套用通用 JWT Profile 而自行補造 Provider 沒有承諾的欄位。這條路徑改用 AWS Cognito 的 token_use=access 區分 Access Token 與 ID Token,再檢查 Audience、Client 與 Scope。

M2M Token 代表 Scheduler,不代表某個人

Scheduler 不經過 Browser、Callback 或 Human Consent。它使用另一個 Confidential App Client,以 Client Credentials 取得 App-only Token:

app client: sre-scheduler
secret:     required
grant:      client_credentials
scope:      platform/observability.query

AWS Cognito M2M 文件 將這條路徑限制在 Resource Server 定義的 Custom Scope。Token Endpoint 只回 Access Token,不回 ID Token 或 Refresh Token。這份 Lab 使用的 Claims 也刻意維持最小集合:

{
  "iss": "https://cognito-idp.ap-northeast-1.amazonaws.com/ap-northeast-1_LabPool",
  "client_id": "sre-scheduler",
  "scope": "platform/observability.query",
  "token_use": "access"
}

這裡沒有登入者,Audit 的 Human 欄位應記成 NOT_APPLICABLE,Machine Actor 則由驗證過的 client_id 映射成 client/sre-scheduler。即使未來 Token 多出 sub,也不能只憑欄位名稱把 Machine Subject 當成人類身分。

M2M 路徑同時多了一份長期責任:Client Secret 必須保存、輪替與停用。AWS Cognito App Client 可以重疊保留新舊 Secret,讓 Workload 先切換到新值,再撤掉舊值。不過把 Secret 放進 Kubernetes Secret 並不代表問題已經解決。如果它曾進入 Image Layer、Git、Terminal History 或權限過寬的 Terraform State,外面再包一層 Secret Object 也補不回已經外洩的 Credential。

Gateway 依 Client 分流 Policy

AWS Cognito 對 Resource Binding 的限制讓兩份 Policy 無法再假裝相同。Human User Flow 可以要求 API-specific aud,Client Credentials M2M 不支援這個參數。因此共同驗證層只處理兩邊都成立的條件,再依已驗證的 client_id 進入不同的 Authorization Rule:

共同條件:signature + iss + exp + token_use=access

Human:client_id + sub + aud + scope
M2M:  client_id + scope,Human 不適用

圖中比較的是雙路徑設計。team 是應用可另外提供的自訂屬性,不是 Cognito 預設 claim,也未作為下面兩條範例 Rule 的條件。

同一個 AWS Cognito issuer 下的 Human 與 M2M 雙路徑。Human 使用 public app client、PKCE 與 resource-bound audience,M2M 使用 confidential app client、custom scope 與 verified client_id,兩者在單一 agentgateway 以 conditional policy 分流。

在 agentgateway 設定裡,aud 不能放進兩條路徑共同必填的 Claims。兩條 CEL Rule 先確認 token_use == "access",再各自處理 Human 與 M2M:

mcpAuthorization:
  rules:
    - >-
      jwt.token_use == "access" &&
      jwt.client_id == "sre-console" &&
      has(jwt.sub) &&
      jwt.aud == "https://observability.lab.example/mcp" &&
      jwt.scope.split(" ").exists(s, s == "platform/observability.query")
    - >-
      jwt.token_use == "access" &&
      jwt.client_id == "sre-scheduler" &&
      !has(jwt.aud) &&
      jwt.scope.split(" ").exists(s, s == "platform/observability.query")

Scope 在 AWS Cognito Access Token 裡是以空白分隔的字串,Policy 要檢查其中是否包含指定值,不能把整串 Scope 做 Equality。requiredClaims 與 Authorization Policy 也負責不同事情。前者確認標準 Claim 是否存在,Provider-specific Claim 的值與路徑分流應留在 CEL Rule。

這份範例明確拒絕帶有意外 aud 的 M2M Token,因為本篇沒有使用 Pre-token Trigger 改寫它。若平台未來決定替 M2M 加入 Audience,Token Contract、Gateway Policy 與 Regression Test 都要一起更新,不能只改 IdP 後就期待 Gateway 自行理解新語意。

完整 Gateway 設定放在 agentgateway-cognito.yaml。公開 Lab 使用合成 Token 與離線 validator 對照分流,這份 agentgateway 設定在 1.4.1 只執行 --validate-only。這能確認設定可被解析,不能證明 Gateway 已用它驗過真實 Cognito Token。Managed Login、JWKS 輪替與 Gateway 的實際請求仍需連到可拋棄的 AWS 環境驗證。

Terraform 只負責固定兩份 Registration

Terraform 範例將 Human 與 M2M 建成兩個獨立 aws_cognito_user_pool_client。Human 設定 generate_secret=false 與 Authorization Code,M2M 則設定 generate_secret=true 與 Client Credentials。完整 HCL 放在 cognito-terraform,包含兩個 App Client 的完整設定。

這份範例只執行 terraform validate,沒有對 AWS Apply。正式套用前還要處理 AWS 權限、Domain 與 Federation 設定。generate_secret=true 也會讓 M2M Secret 進入 Terraform State,因此 Remote State 的加密與存取權限不能省略。

用合成 Token 對照兩份接受條件

最容易出錯的不是兩條成功路徑,而是把 Human 規則套到 Scheduler。Human 的 sub、aud 與 Scope 可以把登入者和目標 API 對起來。M2M 應由已驗證的 client_id 表示。若把 aud 放進兩者共用的必填欄位,合法排程會被拒絕。若為了排程移除所有 Audience 條件,Human Token 的目標邊界也會跟著鬆開。

Day 12 的合成 Token 對照 跑過這兩條路,也刻意讓 Public Client 嘗試 M2M、讓 M2M 要求不支援的 Resource Binding。結果把前面設定差異變成看得到的拒絕位置。完整指令、九組案例與原始紀錄留在 Lab README 和結果檔,日後盤點新 App Client 可直接使用 Human/M2M 雙路徑盤點表。

Day 12 合成 Token 的離線分流結果:Human、M2M 各有成功路徑。錯誤的 Client Type、Resource Binding 或 Policy Input 會被拒絕。

Token 驗過之後,還有平台責任要接

按照這份設計,Human 與 M2M 共用 AWS Cognito Issuer 與 JWKS,各自保留 App Client、Audience Rule、Credential Lifecycle 與 Audit Principal。離線對照能檢查兩份接受條件,正式 Gateway 是否照合約執行,仍要以實際請求驗收。

MCP Client 在送出 Token 前,仍要找到 Authorization Server,也要取得可用的 Client Registration。Resource Server Only Mode 不會自動替平台處理 Pre-registration、Client Metadata、Discovery 與 Provider Adapter。這些工作由誰維護,已經不是單一 JWT Rule 能回答的問題。

下一篇會把視角從 Token 拉回平台選型。我們當初從 LiteLLM 轉向 agentgateway,真正改變決定的不是又多了一個 Routing 功能,而是 Kubernetes 交付、Declarative API、Policy、Audit、Discovery 與升級責任開始一起進入 Operating Model。


上一篇
Day 11|Agent OAuth Flow 實測:PKCE、Client Credentials 與 Token Exchange
下一篇
Day 13|從 LiteLLM 轉向 agentgateway:官方文件沒寫的維運成本
系列文
當企業導入 Agent:30 天拆解治理邊界與平台選型 共 30 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言