ADR-010:OIDC 接入策略 — Authorization Code + PKCE + BFF Pattern + Member Center
狀態
Accepted — 2026-04-26
背景 (Context)
Phase 0 雛形採 StaticAuthProvider(任何帳密都通過、永遠回 demo-user)— 由 ADR-005 規範。雛形已交付(Phase 0 + Phase 0.5 全綠),現在進到 Phase 0.6:
- 真實使用者是 Phase 1 的前置條件 — 沒有真 user 就無法做多用戶測試、無法上線給內部 FAE 試用
- 同期 Innovedus Member Center 已可用 — C# .NET Core + OpenIddict + PostgreSQL,已實作 OAuth Authorization Code + PKCE + JWKS + OpenID Connect Discovery
- 跨產品 SSO 是 Innovedus 集團方向 — visionA、kneron_model_converter、未來其他產品線共用一套帳號
Phase 0.6 必須決定:
- 接什麼 Auth? 自刻 / 第三方 vendor / Member Center
- OAuth flow 怎麼跑? SPA + PKCE / BFF / Implicit Flow
- frontend 怎麼處理 token? localStorage / cookie / server-side session
- dev 環境怎麼起? mock OIDC / 真 Member Center
約束條件
- visionA-frontend 是 Next.js(App Router),目前 token 存 localStorage(已標為 Phase 1 必還的安全債)
- visionA-backend 是 Go(Gin),目前無 cookie session 機制
- Member Center 強制 OAuth client 必須是 confidential(要求 client_secret)— 這就排除了 SPA + PKCE
- Member Center 雛形 OAuth client 註冊機制有
usage 欄位 limitation(無 web_app 類型,需用 webhook_outbound 暫代)
- visionA Agent(local-agent)的 Pairing Token 流程已實作且驗證 OK,不應動到
- 雛形階段使用者是內部開發者 + 內部 FAE,可接受重啟即重登
決策 (Decision)
採 OAuth 2.0 Authorization Code Grant + PKCE + BFF Pattern + Innovedus Member Center。
具體做法
1. 流程:Authorization Code + PKCE redirect
- 標準 OAuth 2.1(PKCE 是 Authorization Code 的 RFC 7636 強化)
code_challenge_method=S256
- 三個隨機值(PKCE verifier、CSRF state、OIDC nonce)由 backend 產生並存 server-side pending session
2. Pattern:BFF(Backend-for-Frontend)
- visionA-backend 是 OIDC confidential client(持有 client_secret)
- visionA-backend 處理完整 OIDC dance:產 PKCE → 302 to MC → 接 callback → 換 token → 驗 id_token → 建 cookie session
- visionA-frontend 完全不接觸 access_token / id_token / refresh_token
- visionA-frontend 只看到一個
visiona_session cookie(HttpOnly + Secure + SameSite=Lax)
3. Identity Provider:Innovedus Member Center
- 不用 Auth0 / Cognito / Clerk — 集團內部解,跨產品 SSO
- dev 環境:直接用真 Member Center(docker-compose 一鍵起 postgres + member-center + visionA-backend + visionA-frontend)
- 不寫 mock OIDC server — 多套程式碼維護,與真實環境差異會藏 bug
4. Session 管理:In-memory + Cookie
- 雛形:
InMemoryStore 在 visionA-backend 進程內持有
- Cookie 內容是
<session_id>.<HMAC-SHA256(session_id)>,server side 才能對應到 user / token
- 重啟即消失(雛形 Phase 0.6 可接受,內部測試者)
- Phase 1 換 Redis / DB(接同 interface)
5. 完全取代 StaticAuthProvider
internal/auth/static.go + static_provider.go 移除
- 不保留 dev fallback(不做
if env=dev then StaticAuth else OIDC 切換)
- 開發者要登入就要起 Member Center,避免 dev / staging / prod 行為分歧
- env var
VISIONA_AUTH_MODE=oidc 預設;static 仍保留以備未來需要
6. 不動 Pairing Token / Agent
- 既有 Pairing Token + Session Token + Agent 流程完全不動
- 改 OIDC 後
UserContext.UserID 從 "demo-user" 變成 OIDC sub(UUID),其他 handler 邏輯不變
- Agent 端不知道 user 是誰、不接 OIDC
7. 順便清三個前端安全債
security.md §14.1 / §14.2 / §14.3 標記的三個雛形安全債(localStorage token、無 refresh、WS querystring token)— 改 OIDC 同時解掉:
- Token 在 backend,不存 localStorage
- 雛形 Phase 0.6 不做 refresh token rotation(Member Center 暫無),但因為 cookie 7 天 TTL + 24h idle,使用者不會頻繁重登
- WS 連線 cookie 自動帶(同 domain),不需 querystring
考慮過的替代方案
方案 A:SPA + PKCE(public client,frontend 持 token)
| 項目 |
評估 |
| 可行性 |
❌ Member Center 強制 confidential client,無法做 |
| 優點 |
backend 簡單;標準 SPA 模式 |
| 缺點 |
Token 在 browser,被 XSS 偷的風險高;MC 不支援 |
| 排除原因 |
Member Center 不支援 public client,硬性排除 |
方案 B:Auth0 / Cognito / Clerk(第三方 vendor)
| 項目 |
評估 |
| 優點 |
現成 UI、社交登入、MFA、密碼重設都做好 |
| 缺點 |
Vendor lock-in;MAU 增加成本線性上升;跨 Innovedus 產品 SSO 需要他們的企業方案;資料外流到第三方 |
| 排除原因 |
與「跨 Innovedus 產品線統一 SSO」目標衝突;長期成本與資料治理風險 |
方案 C:自刻 OAuth + JWT
| 項目 |
評估 |
| 優點 |
完全掌控 |
| 缺點 |
要自己做密碼重設、email 驗證、2FA、暴力破解防禦;維運成本高;安全風險自承 |
| 排除原因 |
重複造輪子;Member Center 已存在且專為此而生 |
方案 D:繼續用 StaticAuthProvider
| 項目 |
評估 |
| 優點 |
不用做事 |
| 缺點 |
無法多用戶測試;無法進 Phase 1 |
| 排除原因 |
Phase 0.6 的目的就是要升級 |
方案 E:Implicit Flow(OAuth 2.0 舊版)
| 項目 |
評估 |
| 優點 |
簡單,無需 token endpoint |
| 缺點 |
OAuth 2.1 已 deprecated;token 直接在 URL fragment,安全性差 |
| 排除原因 |
業界共識:不要用 Implicit Flow |
方案 F:BFF 但 IdP 用 Keycloak / Ory Kratos 自架
| 項目 |
評估 |
| 優點 |
開源、標準、可控 |
| 缺點 |
多一套服務要維運;與 Member Center 重複定位 |
| 排除原因 |
Innovedus 已選定 Member Center,沒理由再起一套 |
後果 (Consequences)
正面影響
- 跨 Innovedus 產品 SSO:使用者一組帳號用所有 Innovedus 產品
- 安全性提升:BFF 把 token 守在 backend,順便清三個前端安全債(§14.1 / §14.2 / §14.3)
- frontend 簡化:不用做 PKCE、不用管 token 刷新、不用處理 callback;登入按鈕變成一個
<a href>
- 與業界對齊:BFF + Authorization Code + PKCE 是 OAuth 2.1 推薦做法
- dev 環境真實:直接接真 Member Center,無 mock 與 prod 行為差異
- Agent 流程零影響:Pairing / tunnel 完全不動
負面影響(接受的取捨)
- Backend 多一塊責任:cookie session store + OIDC client + JWKS cache(已寫進
internal/oidc/ + internal/usersession/)
- dev 必須起 Member Center:開發者第一次 setup 多一步(
docker compose up);用 Makefile 一鍵化緩解
- 重啟即消失:雛形 in-memory session 重啟 → 全使用者重登(Phase 0.6 階段內部測試可接受;Phase 1 上 Redis)
- 依賴 Member Center 上線:MC 掛掉 visionA 也無法登入(Phase 1 需考慮 MC 的 SLO + circuit breaker)
- Member Center
usage limitation:雛形暫用 usage=webhook_outbound,命名語意不對;需在 MC 開 issue 加 usage=web_app
風險
| 風險 |
緩解 |
| Member Center dev 環境不穩 → 影響 visionA 開發 |
docker-compose 固定版本;Member Center 團隊維持基礎可用性 |
| Member Center 改 schema / API → visionA 跟著爆 |
用 OIDC 標準介面(discovery + JWKS)— Member Center 改實作不改介面就 OK |
| Cookie SameSite 在某些瀏覽器舊版有兼容問題 |
雛形支援的瀏覽器是現代版 Chrome/Firefox/Safari/Edge,無此問題 |
| OIDC client_secret 外洩 |
env / Secrets Manager + 不 commit + log 不印 secret + 可隨時 rotate |
| Member Center webhook(用戶刪除 / 停用通知)未實作 |
雛形不接收;Phase 1 補(記入 TODO) |
usage=webhook_outbound 之後 MC 真的有 webhook 場景時撞名 |
開 issue 推 MC 加 usage=web_app;切換無需動 visionA 程式碼 |
合規性
相關文件
- 上位:
adr/adr-005-no-db-auth-in-prototype.md(Auth 部分被本 ADR 推翻;DB 部分仍有效)
- 同層:
adr/adr-011-supersede-static-auth.md(OB6 任務時建立;明確記錄推翻 ADR-005 Auth 部分)
- 詳細實作:
oidc-tdd.md(Phase 0.6 OIDC TDD 增補)
- 安全:
security.md §2、§14(前端安全債清單)
- 既有 Auth 設計:
TDD.md §2.2(AuthService + AuthProvider 雙層 interface)
版本記錄
| 日期 |
版本 |
變更 |
| 2026-04-26 |
1.0 |
初版 — 反映 Phase 0.6 七個議題的使用者裁決 |