# ADR-011:推翻 ADR-005 — 雛形階段升級接 Innovedus Member Center ## 狀態 Accepted — 2026-04-26 ## 推翻 [ADR-005](./adr-005-no-db-auth-in-prototype.md) — Auth 部分(DB 部分仍有效) ## 背景 (Context) ADR-005(2026-04-21)在 Phase 0 雛形階段決定**不接 auth**: - 用 `StaticAuthProvider` 任何帳密都通過、永遠回 `demo-user` - `StaticAuthService` middleware 永遠注入 demo-user UserContext - 雛形 PRD / TDD 中所有 user-bound 資源(Device / Model / PairingToken)都綁到 demo-user 當時的決策邏輯: - 雛形階段目標是驗證「雲端端對端連通」技術可行性 - 把時間花在 Auth、user 系統、DB schema 上會延誤核心驗證 - 介面(`AuthProvider` / `AuthService`)已切乾淨,未來換實作零業務邏輯改動 到了 Phase 0.6(2026-04-26),情況變了: - 雛形已交付(Phase 0 + Phase 0.5 全綠),核心架構通過驗證 - 需要進入 Phase 1(多用戶、上線給 FAE 試用)— 沒真實 user 寸步難行 - 同期間 Innovedus 集團另一條線 **Member Center 已可用**(C# .NET Core + OpenIddict + PostgreSQL,70% 完成度,OAuth Authorization Code + PKCE + JWKS 都能跑) - 跨產品 SSO 是 Innovedus 集團方向(visionA / kneron_model_converter / 未來其他產品線) 繼續用 StaticAuth 的代價: - 無法做多用戶測試(內部 FAE 試用無法區分使用者) - 無法把雛形交給內部使用者(資料會混在一起) - 之後要接 OIDC 還是要做 → 不如現在做完,後面少一輪改動 ## 決策 (Decision) **推翻 ADR-005 的 Auth 部分**:Phase 0.6 起 visionA-backend 的唯一認證路徑改為 OIDC,接 Innovedus Member Center。 ### 具體做法 1. **拔除 StaticAuthProvider / StaticAuthService** - 刪除 `internal/auth/static.go` + `static_test.go` - `internal/api/middleware.go` 的 `AuthMiddleware` 拿掉雙模式分支,只剩 OIDC - `internal/api/api.go` 的 `Deps` 拿掉 `AuthService` / `AuthProvider` 欄位;`validate()` 強制 `OIDCProvider` + `SessionManager` 必填(缺則啟動 panic) - `cmd/api-server/main.go` 拿掉 `cfg.OIDC.Enabled` 旗標分支,OIDC 變成必須路徑 2. **OIDC 實作**(OB1-OB4 已完成;本 ADR 只記錄決策) - 採 OAuth 2.0 Authorization Code + PKCE + BFF Pattern(詳見 ADR-010) - visionA-backend 是 OIDC confidential client(持有 client_secret) - frontend 完全不接觸 token,只看到 `visiona_session` cookie - In-memory session store(重啟即消失,雛形階段可接受) 3. **保留 AuthProvider / AuthService interface** - 雖然 Static 實作已刪,interface 本身仍保留在 `internal/auth/auth.go` - 給未來新增備援 provider(Phase 1 backup local auth、service-to-service token)使用 - 介面代表 contract — 提早設計、提早被驗證的介面比 ad-hoc 重新發明便宜 4. **Pairing Token 流程不動**(ADR-005 的另一個面向) - PairingStore / SessionTokenStore 仍是 in-memory + interface 抽象 - 唯一改變:`UserContext.UserID` 從固定的 `"demo-user"` 變成 OIDC `sub`(UUID),自然穿透到 `PairingStore.Create` 的 user_id 參數 - Agent 端不知道 user 是誰、不接 OIDC 5. **dev 環境策略** - 不寫 mock OIDC server(測試例外 — 見 `internal/oidctest`) - 開發者要登入就得起 Member Center(docker-compose 一鍵化) - 為了讓 demo-user 流程繼續可用,Member Center 端會 seed `demo@visionA.local / demo123` 帳號 ### 仍維持 ADR-005 規範的部分 - **不接真實 DB**:Device / Model / Pairing 仍用 InMemoryRepository - **Repository interface 抽象**:Phase 1 換 PostgresRepository 仍然零業務邏輯改動 - **資料重啟會遺失**:雛形 process 重啟 → user session、device、model、pairing token 全消失 ## 考慮過的替代方案 | 方案 | 評估 | 排除原因 | |------|------|---------| | **繼續用 StaticAuth 到 Phase 1** | 不用做事 | 無法多用戶測試;FAE 試用會撞牆;之後還是要接 | | **保留 dev fallback 切換** | dev 環境不需起 Member Center | 各環境行為分歧難排查;且 Member Center docker-compose 一鍵起,成本不高 | | **接 Auth0 / Clerk / Cognito** | 現成 | 跨 Innovedus 產品 SSO 需企業方案;vendor lock-in;資料外流 | | **自刻 email + password + JWT** | 完全掌控 | 要做密碼重設、2FA、暴力破解防禦;維運成本太高 | 詳細替代方案分析見 [ADR-010](./adr-010-oidc-bff.md)。 ## 後果 (Consequences) ### 正面影響 - **跨 Innovedus 產品 SSO**:使用者一組帳號用所有 Innovedus 產品線 - **真實 user binding for Pairing token**:多使用者上線時不再混淆 - **進 Phase 1 的前置就緒**:FAE 內部試用、多用戶測試、上線都有 Auth 基礎 - **frontend 安全性提升**:Token 在 backend,不存 localStorage(順便清掉 security.md §14.1 / §14.2 / §14.3 三筆雛形安全債) - **demo-user 流程繼續可用**:Member Center seed 同名帳號,雛形 demo 體驗不變 - **程式碼更乾淨**:拔除「雙模式 if/else」之後 auth middleware 邏輯線性、容易追蹤 ### 負面影響(接受的取捨) - **dev 環境多一個依賴**:起 visionA 要先起 Member Center + Postgres(docker-compose 緩解) - **對 Member Center dev 環境穩定性有依賴**:MC 掛掉 visionA 也無法登入 - 緩解:docker-compose 固定版本;Member Center 團隊維持基礎可用性 - **session 重啟即消失**:in-memory store;雛形 Phase 0.6 可接受,內部測試者 - Phase 1 換 Redis/DB(同 interface) ### 風險 | 風險 | 緩解 | |------|------| | Member Center API 改動 → visionA 跟著爆 | 用 OIDC 標準介面(discovery + JWKS) — Member Center 改實作不改介面就 OK | | Member Center webhook(user 刪除 / 停用)未實作 | 雛形不接收;Phase 1 補(記入 oidc-tdd.md TODO) | | OIDC client_secret 外洩 | env / Secrets Manager + 不 commit + log 不印 secret + 可隨時 rotate | | 既有 Pairing flow 對 user_id 變動的破壞 | OB5 整合測試 `TestOIDCE2E_PairingTokenBindsToOIDCUser` + `TestOIDCE2E_MultiUserIsolation` 驗證 user binding 正確 | ## 合規性 - [x] Architect 確認 - [x] 使用者裁決 Q1(OIDC + PKCE redirect)— 見 ADR-010 - [x] 使用者裁決 Q4(完全取代 StaticAuth,不保留 dev fallback)— 見 ADR-010 - [x] OB5 任務完成:StaticAuth 已從 codebase 移除 - [x] OB5 任務完成:所有測試(含原 build-tag 隔離的 oidc_e2e_test.go)皆通過 - [ ] Phase 1 Kick-off:補 user 表 + 接 DB(取代 in-memory session store) ## 相關文件 - 上位:[ADR-005](./adr-005-no-db-auth-in-prototype.md)(被本 ADR 推翻 Auth 部分;DB 部分仍有效) - 同層:[ADR-010](./adr-010-oidc-bff.md)(OIDC 接入策略 — 為什麼選 BFF + Authorization Code + PKCE + Member Center) - 詳細實作:[oidc-tdd.md](../oidc-tdd.md)(Phase 0.6 OIDC TDD 增補) - 安全:[security.md](../security.md) §2、§14(前端安全債清單) ## 後記(2026-05-01) Phase 0.7 stage 部署時發現 Innovedus stage Member Center 配給 visionA 的 login OAuth client 是 **public PKCE-only**(無 `client_secret`),與本 ADR §32 中「visionA-backend 是 OIDC confidential client(持有 client_secret)」的假設不符。 具體狀況: - Stage MC 端為 redirect flow 配的是 public client(`b8093fea1a504a5d8f0e04bee9f78f2e`,無 secret) - 另配一組 confidential client(`` + secret)給未來 client_credentials grant 用,**login flow 不用此組** - 表示 ADR-010 §「MC 強制 confidential」前提對 redirect flow 不再成立(對 server-to-server flow 仍成立) 處理: - visionA-backend 擴充為兩種 mode 都支援(ClientSecret 從必填變選填,runtime 判斷走哪條路) - 詳細決策見 [ADR-013](./adr-013-oidc-public-pkce-client.md) - 本 ADR 的核心結論(OIDC 取代 StaticAuth、保留 AuthProvider/AuthService interface、Pairing 流程不動、in-memory session)**全部仍有效**,只是 client 配置層多了一個維度 ## 版本記錄 | 日期 | 版本 | 變更 | |------|------|------| | 2026-04-26 | 1.0 | 初版 — OB5 完成後正式記錄推翻 ADR-005 Auth 部分 | | 2026-05-01 | 1.1 | 新增「後記」段:stage 部署發現 MC login client 為 public PKCE-only,擴充由 ADR-013 處理 |