visionA/docs/autoflow/04-architecture/adr/adr-013-oidc-public-pkce-client.md
jim800121chen fb7da5d180 chore(autoflow): migrate .autoflow/ 共享層文件至 docs/autoflow/
依 autoflow-agent workspace v2 設計把 PRD / 設計 / 架構 / 交付類
共享文件從個人層 .autoflow/(ignored)搬到 docs/autoflow/(進 git),
讓團隊可共享產品與架構文件,個人層只留 progress / review / testing 等
per-branch 筆記。

- 02-prd/        21 個檔(PRD、features、market-analysis 等)
- 03-design/     18 個檔(design-spec、wireframes、flows 等)
- 04-architecture/ 31 個檔(TDD、design-doc、ADR×14、API 規格等)
- 07-delivery/   3 個檔(project-summary、phase-0.6-handover、stage-deployment-setup)

合計 73 檔。原檔已從 .autoflow/ 移除(migration 工具執行 git mv,
但因 .autoflow/ 在 .gitignore 中、git 將此操作視為新增、無 rename history)。
2026-05-04 16:55:55 +08:00

12 KiB
Raw Permalink Blame History

ADR-013OIDC 支援 Public PKCE-only ClientClientSecret 變選填)

狀態

Accepted — 2026-05-01

上位文件

  • ADR-010OIDC 接入策略 — BFF + Authorization Code + PKCE
  • ADR-011OB5 拔除 StaticAuth、強制 OIDC
  • oidc-tdd.md §13.1(環境變數)

推翻

無 — 本 ADR 是對 ADR-010 的擴充(在原 confidential client 路徑外新增 public client 路徑),不是推翻。

背景 (Context)

ADR-010 在 2026-04-26 決定 visionA-backend 是 OIDC confidential client(持有 client_secret),理由是:

Member Center 強制 OAuth client 必須是 confidential要求 client_secret— 這就排除了 SPA + PKCE。

到了 2026-05-01 Phase 0.7 stage 部署時情況變了。Innovedus 集團的 stage Member Centerhttps://stage-9527.innovedus.com:7850/)配給 visionA 的 OAuth client 實際是兩組:

Client Client ID Secret 用途
Loginpublic PKCE-only b8093fea1a504a5d8f0e04bee9f78f2e OAuth Authorization Code + PKCE redirect flow
Service-to-serviceconfidential <see stage .env.stage> <see stage .env.stage, never commit> client_credentials grant — visionA-backend 直接打 MC APIPhase 1 才會用)

🔒 Secret hygiene 註記2026-05-01 補):本 ADR 初版誤將真實 stage service client 的 id 與 secret 寫死進此表格review 階段發現後立刻移除。原始 secret 未進 git 歷史(檔案從未被 commit、git log 已確認),但仍視為「曾於工作目錄中存在」處理 — 請 MC 團隊將該 service client_secret rotate 後再啟用。Login client_id 是 OAuth public 資訊spec 不視為 secret保留可接受service client 兩個值真正注入只發生在 stage host 的 .env.stage(已加入 .gitignore)。

也就是說 ADR-010 的前提「MC 強制 confidential」對 login flow 不再成立 — MC 為 redirect flow 配的是 public client靠 PKCE 防 code interception沒給 secret。

直接後果visionA-backend 啟動時會 panic — internal/config/config.goValidate()OIDC.ClientSecret 列為必填env 沒給就 fail-fast。要在 stage 跑起來必須改。

觸發條件

  • A1 任務Phase 0.7)需要把 stage MC 給的 public PKCE-only login client 跑起來
  • 同時要保留現有 dev / 未來 prod 可能用到的 confidential client mode向下相容
  • 為未來 Phase 1 接 MC API用 client_credentials預留 config 鉤子,但不啟用實作

為什麼 PKCE-only public client 對 redirect flow 是夠的

OAuth 2.1RFC 9700 BCP+ RFC 8252 對「無法安全儲存 client_secret 的 client」SPA、native app、且本案 — 雖然 visionA-backend 是 server但 MC 把它分類為 redirect-flow client的標準做法

  1. PKCERFC 7636防 authorization code interception:攻擊者就算攔截到 authorization code沒有 code_verifier 換不到 token
  2. code_challenge_method=S256 強制visionA 本來就只用 S256
  3. state 防 CSRFvisionA pending session 已實作)
  4. nonce 防 id_token replayvisionA pending session 已實作)

OWASP / IETF OAuth WG 結論(截至 2026對 redirect flowPKCE 是必備、client_secret 對 public client 不是必備。confidential client 的 secret 主要是給「沒 user redirect」的 server-to-server flowclient_credentials、refresh_token 等)做 client authentication 用的redirect flow 有 PKCE 已經夠。

換句話說visionA login 路徑從 confidential 變 public安全等級沒掉太多 — 主要差異在 token endpoint 不附 client_secret但 PKCE 守住了 code interception 這個主要威脅。

決策 (Decision)

OIDC.ClientSecret 從必填變選填。visionA-backend 同時支援兩種 mode

Mode 觸發條件 Token endpoint 行為 安全模型
Public PKCE-onlystage、未來可能 prod VISIONA_OIDC_CLIENT_SECRET 為空 oauth2.Config.ClientSecret = ""golang.org/x/oauth2 lib 自動不送 client_secret PKCE + state + nonce
Confidentialdev、向下相容 VISIONA_OIDC_CLIENT_SECRET 非空 oauth2.Config.ClientSecret = <secret>,正常 client_secret_post / basic auth PKCE + state + nonce + client_secret

具體做法

1. internal/config/config.go

  • OIDCConfig.ClientSecret 欄位保留,但 godoc 改為「選填 — 為空時走 public PKCE-only mode」
  • Validate()VISIONA_OIDC_CLIENT_SECRET 從 missing 檢查清單移除
  • 其他 OIDC 必填欄位IssuerURL / ClientID / RedirectURL / PostLoginURL / SessionSecret不變

2. internal/oidc/provider.go

  • validateConfig()ClientSecret 從必填檢查移除
  • oauth2.Config.ClientSecret 直接傳 cfg.ClientSecret(空字串時 oauth2 lib 會自動省略該欄位,行為符合 RFC 6749 §3.2.1「The client MAY omit the parameter」
  • IDTokenVerifier 不變(它只看 ClientID 做 audience check與 secret 無關)

3. 預留 service client config

新增兩個 config 欄位(Phase 0.7 不實作 client_credentials flow,只是預留 env 不讓使用者設了沒用):

type OIDCConfig struct {
    // ...既有欄位
    
    // ServiceClientID / ServiceClientSecret 是 client_credentials grant 用的
    // confidential client給 visionA-backend 主動打 Member Center API例如查
    // user metadata、發 webhook。Phase 0.7 不實作config 欄位先佔位,避免未來
    // 加新 env 時要再過一輪文件 review。
    ServiceClientID     string // VISIONA_OIDC_SERVICE_CLIENT_ID選填
    ServiceClientSecret string // VISIONA_OIDC_SERVICE_CLIENT_SECRET選填
}

Validate() 不檢查這兩個欄位 — 兩個都空Phase 0.7 / stage 不接 MC API也合法。

4. 文件更新

  • oidc-tdd.md §13.1 把 VISIONA_OIDC_CLIENT_SECRET 標為「選填」
  • oidc-tdd.md §13.1 新增 dev / stage / prod 三種 client 模式的 env 範例段
  • ADR-011 後記補一段,註記 stage 部署時發現 MC 給 public client 的事實

5. 向下相容

既有 dev docker-composedocker-compose.dev.ymlseed demo@visionA.local 帳號 + confidential client 完全不動,繼續走 confidential mode。沒有 breaking change。

考慮過的替代方案

方案 A保留 confidential-only請 MC 改發 confidential client 給 stage

項目 評估
優點 不用動 visionA 程式碼
缺點 違反 MC 的 client 分類設計login client 就應該是 public PKCE-only要等 MC 排期改;阻塞 stage 部署
排除原因 MC 的設計是對的symmetric 的事 OIDC 標準也偏好 public client for redirect flow不該為了省一次 visionA 文件更新而扭曲 IdP 設計

方案 BvisionA 自帶一組 client_secret硬塞 MC

項目 評估
優點
缺點 MC 端不認token endpoint 會 401unauthorized client
排除原因 技術上跑不起來

方案 Cdev 用 confidential、stage 用 public、用 build flag 切換

項目 評估
優點 編譯期保證每個環境只有一條路徑可走
缺點 +build tag 增加 CI 複雜度;本質上是 runtime 決定的事不該綁 build flag違反 12-factor「同一個 binary 跑各環境」
排除原因 過度工程ClientSecret 是否為空是天然的 runtime switch

方案 D刻意不向下相容全部改用 public client

項目 評估
優點 程式碼稍微簡單
缺點 dev 既有 docker-compose seed 流程要全改;未來 prod 若要強化confidential + PKCE 雙保險做不到ADR-010 的決策被無故推翻
排除原因 本案是擴充不是推翻,沒理由減功能

方案 E採用ClientSecret 變選填runtime 判斷走哪條路

項目 評估
優點 同一份程式碼 / binary 跑 dev / stage / prod 三種環境最小改動oauth2 lib 原生支援空 secret向下相容
缺點 要在文件 + ADR 多寫一段說明何時用哪 mode
採用原因 正確抽象confidential vs public 是 client 配置屬性而非系統屬性;應由 IdP 註冊時決定visionA-backend 跟隨

後果 (Consequences)

正面影響

  • stage 部署解鎖 — A1 改造後 visionA-backend 可直接吃 stage MC 給的 public PKCE-only client
  • 同一份 binary 多環境 — dev confidential / stage public / prod 視 IT 配置 — 不需重 build
  • 跟隨 OAuth 2.1 趨勢 — IETF / OWASP 對 redirect flow 偏好 public PKCE-onlyconfidential 主要留給 server-to-server
  • 向下相容 — dev 既有流程零改動
  • 預留 service client 鉤子 — Phase 1 接 MC APIclient_credentials時 config 欄位已經在,不必再改 ADR

負面影響(接受的取捨)

  • 失去一層防護public mode 沒了 client_secrettoken endpoint 對「冒充 visionA client_id 的攻擊者」沒防護。緩解PKCE 守住主要威脅code interceptionMember Center 端會用 redirect_uri 白名單加碼防護
  • 文件複雜度上升oidc-tdd.md §13.1 要多寫一段「dev / stage / prod 的 client mode 差異」
  • 可能誤設:開發者忘記設 VISIONA_OIDC_CLIENT_SECRET 但 MC 端註冊的是 confidential client → token exchange 會 401。緩解oidc-tdd.md §13.1 範例與啟動 log 要明確顯示「啟動時偵測到 ClientSecret 為空,走 public PKCE-only mode」

風險

風險 緩解
Member Center 改變 stage client 類型public ↔ confidential 切換)導致 visionA 起不來 env 兩種 mode 都支援;切換只需改 .env.stage 加 / 移除 VISIONA_OIDC_CLIENT_SECRET,不需重 build
開發者誤把 dev 的 confidential secret 帶進 stage env 透過 stage host 的 .env.stage 不進 git + secrets reviewsecret 本身對 public client 是無害冗餘MC 會忽略),不會立即出問題
oauth2 lib 版本升級行為改變client_secret 空字串時行為) 在 backend test 加 TestExchangeCode_PublicClient 確認空 ClientSecret 走通CI 跑得到
攻擊者拿到 stage public client_id 自己模擬 visionA → 但 redirect_uri 必須符合 MC 白名單,且 PKCE verifier 守住 redirect_uri 白名單由 MC 管理(已是 stage 配置PKCE 由 visionA-backend 每次新產
Phase 1 同時用 redirect public client + service confidential client兩組 secret 管理混淆 config 欄位命名清楚(ClientSecret vs ServiceClientSecretADR 後續更新時記錄職責邊界

合規性

  • Architect 確認2026-05-01
  • 使用者確認 stage MC 配給 visionA 的是 public PKCE-only clientprogress.md OIDC Client 配置表)
  • A1-1 / A1-2 backend 改造從必填變選填、ClientSecret 空走 public mode
  • A1-3 補測試public + confidential 兩種 mode 都綠
  • A1-5 oidc-tdd.md §13.1 + .env.dev.example 文字更新
  • A1-6 預留 ServiceClientID / ServiceClientSecret config 欄位(不啟用實作)
  • V-4 stage 實際走完一次 OIDC flow確認 callback → cookie session 完整成立

相關文件

  • 上位:ADR-010(本 ADR 是其擴充,非推翻)
  • 同層:ADR-011(後記補上 2026-05-01 stage 發現)
  • 詳細實作 / env 規格:oidc-tdd.md §13.1
  • 部署架構:stage-deployment.md(同期建立)
  • OAuth 規格RFC 7636PKCE、RFC 9700OAuth 2.0 BCP、RFC 8252Native Apps BCP

版本記錄

日期 版本 變更
2026-05-01 1.0 初版 — 反映 stage MC public PKCE-only client 配給ClientSecret 變選填;預留 ServiceClient 欄位