依 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)。
12 KiB
ADR-013:OIDC 支援 Public PKCE-only Client(ClientSecret 變選填)
狀態
Accepted — 2026-05-01
上位文件
- ADR-010(OIDC 接入策略 — BFF + Authorization Code + PKCE)
- ADR-011(OB5 拔除 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 Center(https://stage-9527.innovedus.com:7850/)配給 visionA 的 OAuth client 實際是兩組:
| Client | Client ID | Secret | 用途 |
|---|---|---|---|
| Login(public PKCE-only) | b8093fea1a504a5d8f0e04bee9f78f2e |
無 | OAuth Authorization Code + PKCE redirect flow |
| Service-to-service(confidential) | <see stage .env.stage> |
<see stage .env.stage, never commit> |
client_credentials grant — visionA-backend 直接打 MC API(Phase 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.go 的 Validate() 把 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.1(RFC 9700 BCP)+ RFC 8252 對「無法安全儲存 client_secret 的 client」(SPA、native app、且本案 — 雖然 visionA-backend 是 server,但 MC 把它分類為 redirect-flow client)的標準做法:
- PKCE(RFC 7636)防 authorization code interception:攻擊者就算攔截到 authorization code,沒有
code_verifier換不到 token code_challenge_method=S256強制(visionA 本來就只用 S256)state防 CSRF(visionA pending session 已實作)nonce防 id_token replay(visionA pending session 已實作)
OWASP / IETF OAuth WG 結論(截至 2026):對 redirect flow,PKCE 是必備、client_secret 對 public client 不是必備。confidential client 的 secret 主要是給「沒 user redirect」的 server-to-server flow(client_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-only(stage、未來可能 prod) | VISIONA_OIDC_CLIENT_SECRET 為空 |
oauth2.Config.ClientSecret = "",golang.org/x/oauth2 lib 自動不送 client_secret |
PKCE + state + nonce |
| Confidential(dev、向下相容) | 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-compose(docker-compose.dev.yml)seed 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 設計 |
方案 B:visionA 自帶一組 client_secret,硬塞 MC
| 項目 | 評估 |
|---|---|
| 優點 | — |
| 缺點 | MC 端不認;token endpoint 會 401(unauthorized client) |
| 排除原因 | 技術上跑不起來 |
方案 C:dev 用 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-only(confidential 主要留給 server-to-server)
- 向下相容 — dev 既有流程零改動
- 預留 service client 鉤子 — Phase 1 接 MC API(client_credentials)時 config 欄位已經在,不必再改 ADR
負面影響(接受的取捨)
- 失去一層防護:public mode 沒了 client_secret,token endpoint 對「冒充 visionA client_id 的攻擊者」沒防護。緩解:PKCE 守住主要威脅(code interception);Member 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 review;secret 本身對 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 ServiceClientSecret);ADR 後續更新時記錄職責邊界 |
合規性
- Architect 確認(2026-05-01)
- 使用者確認 stage MC 配給 visionA 的是 public PKCE-only client(progress.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 7636(PKCE)、RFC 9700(OAuth 2.0 BCP)、RFC 8252(Native Apps BCP)
版本記錄
| 日期 | 版本 | 變更 |
|---|---|---|
| 2026-05-01 | 1.0 | 初版 — 反映 stage MC public PKCE-only client 配給;ClientSecret 變選填;預留 ServiceClient 欄位 |