visionA/docs/autoflow/04-architecture/adr/adr-012-pending-session-shared-cookie.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

99 lines
6.0 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# ADR-012Pending Session 與 Logged-in Session 共用同一個 Cookie
## 狀態
Accepted — 2026-04-21
## 上位文件
- [oidc-tdd.md §4.5](../oidc-tdd.md#45-handler-範例與-pending-session)
- [adr-010-oidc-bff.md](./adr-010-oidc-bff.md)
## 背景 (Context)
OIDC Authorization Code + PKCE flow 在 backend 端會產生兩種 server-side session 狀態:
1. **Pending session**OIDC dance 進行中):`/api/auth/login` 階段建立,存放 PKCE code_verifier、CSRF state、OIDC nonce、return_to。生命週期短≤ 10 分鐘,從使用者點 Login 到 IdP 完成同意UserID 為空。
2. **Logged-in session**(已登入):`/api/auth/callback` 完成後UserID 從 OIDC sub claim 填入pending 欄位清空。生命週期長(雛形 24hPhase 1 設計 7d
`oidc-tdd.md §4.5` 的原始設計示意了**兩個獨立 cookie**
- `visiona_pending_sid`(短 TTL10 分鐘)
- `visiona_session`(長 TTL登入後寫入
雛形實作OB2 / OB4為了減少 cookie 數量、簡化 handler 與 store 邏輯,**改採合一**:兩種 session 共用同一個 `visiona_session` cookie + 同一個 `usersession.Store` record`Session.UserID` 是否為空來區分階段。
OB5 review2026-04-21將此偏離標為 Major-3「technical debt 而非漏洞」,建議 Phase 1 上線前評估是否還原 TDD 原設計。本 ADR 在 review round 2 後做出最終決定。
## 決策 (Decision)
**保留合一設計**pending 與 logged-in 共用同一個 `visiona_session` cookie**不**還原 TDD §4.5 的兩個 cookie 設計。
### 配套防護機制
合一設計的潛在風險pending session 被當成 logged-in 用)由以下三道防線阻擋:
1. **AuthMiddleware 強制檢查 `UserID == ""` → 401 `session_not_authenticated`**
- `internal/api/middleware.go` AuthMiddleware 在拿到 session 後立刻判斷 UserID 是否為空
- 空 → 401pending session 訪問 protected endpoint 一律拒絕)
- 此檢查不可拿掉、不可放寬
- 對應測試:`TestOIDCMiddleware_Rejects_PendingSession`
2. **Callback 完成時 rotate session ID**Fix-A1 / ADR-012 配套)
- `internal/api/oidc_auth.go` callback handler 在驗 id_token 成功後立即呼叫 `usersession.Manager.RotateSessionID`
- 新 session 從一個全新的 random ID 開始,舊 pending session ID 從 store 中刪除
- 防護 session fixationOWASP ASVS V3.2.1
- 對應測試:`TestOIDCCallback_RotatesSessionID_PreventsFixation``TestManager_RotateSessionID_HappyPath`
3. **Pending state 在 callback 完成同一次 UpdateSession 中清空**
- OIDCState / OIDCNonce / OIDCCodeVerifier / Extra["return_to"] 在寫入 user info 的同一次 Update 中清掉
- 確保 logged-in session 不殘留 pending 欄位
## 理由 (Rationale)
### 為什麼維持合一
1. **Cookie 數量少對 frontend 簡單**frontend 只關心一個 cookie 是否存在,不需要區分 pending/logged-in 兩個 cookie 的狀態
2. **Handler 邏輯簡單**`oidcCallbackHandler` 不需要「讀 pending cookie → 建 new logged-in cookie → 清 pending cookie」三步操作改 RotateSessionID 一步搞定
3. **Store 操作減少**:每個 cookie 對應一個 store record兩個 cookie 等於 store 寫入次數加倍
4. **防護機制已到位**上述三道防線middleware UserID 檢查 + RotateSessionID + 同一次清理)涵蓋原本兩個 cookie 設計要解的問題
### 為什麼不拆兩個 cookie
1. **大改動**:要動 `usersession.Manager``oidc_auth.go` 兩處核心 handler、middleware、所有相關測試
2. **不解新風險**pending vs logged-in 的混淆風險已由現有防線阻擋;拆兩個 cookie 只是把同一個保護換個位置實作
3. **Phase 1 Redis 化更複雜**:兩個 cookie 對應兩個 store keyRedis 命名空間 / TTL 管理變複雜;合一設計直接共用一個 key
4. **TDD §4.5 是文件示意**:原設計是教學性的「直覺易懂」呈現,不是經過威脅模型分析的最佳化方案
## 取捨 (Trade-offs)
### 優點
- **Cookie 數量少**browser cookie jar 簡潔DevTools 偵錯清楚
- **邏輯路徑短**handler / middleware / store 都只處理一種 cookie + 一種 store record
- **Phase 1 換 Redis 影響面小**:同一個 Redis key 即可,不需要兩套 namespace
- **測試簡單**:不需要在每個 test 中模擬「兩個 cookie 同時存在 / 其一缺失」的所有組合
### 缺點
- **理論上 pending 與 logged-in 共用 store record**,若 middleware UserID 檢查被誤刪會立刻變漏洞
- **緩解**middleware 該行加註解明確警告不可拿掉CI 可加 grep 檢查
- **`Session` struct 同時有 OIDC pending 欄位 + user info 欄位**序列化時負擔略大Phase 1 Redis 化時感受得到)
- **緩解**Phase 1 評估是否分兩個 struct但仍共用同一個 Redis key + 不同的序列化 schema
- **與 oidc-tdd.md §4.5 文件示意不符**
- **緩解**TDD §4.5 已加註「實際採合一設計,詳見 ADR-012」後續更新
## 影響範圍
| 區塊 | 影響 |
|------|------|
| `internal/usersession/usersession.go` | `Session` struct 同時包含 OIDC pending + user info 欄位(已實作) |
| `internal/usersession/manager.go` | 新增 `RotateSessionID`Fix-A1做為 fixation 防護 |
| `internal/api/oidc_auth.go` | callback 流程state 比對 → ExchangeCode → VerifyIDToken → **RotateSessionID** → set user info → UpdateSession |
| `internal/api/middleware.go` | AuthMiddleware 強制檢查 `UserID == ""` → 401不可拿掉 |
| Phase 1 Redis 化 | pending 與 logged-in 共用同一個 Redis key + 同一個 schema不影響架構 |
## 關聯
- 推翻:無(補充 ADR-010 的實作細節決策)
- 相關:[ADR-010](./adr-010-oidc-bff.md)、[ADR-011](./adr-011-supersede-adr-005.md)
- 配套修復Fix-A1RotateSessionID、Fix-A2本 ADR + middleware 註解)
- 相關 review`.autoflow/05-implementation/review/oidc-G5-OB1-OB6-review.md` Major-3