依 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)。
99 lines
6.0 KiB
Markdown
99 lines
6.0 KiB
Markdown
# ADR-012:Pending 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 欄位清空。生命週期長(雛形 24h,Phase 1 設計 7d)。
|
||
|
||
`oidc-tdd.md §4.5` 的原始設計示意了**兩個獨立 cookie**:
|
||
- `visiona_pending_sid`(短 TTL,10 分鐘)
|
||
- `visiona_session`(長 TTL,登入後寫入)
|
||
|
||
雛形實作(OB2 / OB4)為了減少 cookie 數量、簡化 handler 與 store 邏輯,**改採合一**:兩種 session 共用同一個 `visiona_session` cookie + 同一個 `usersession.Store` record,由 `Session.UserID` 是否為空來區分階段。
|
||
|
||
OB5 review(2026-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 是否為空
|
||
- 空 → 401(pending 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 fixation(OWASP 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 key,Redis 命名空間 / 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-A1(RotateSessionID)、Fix-A2(本 ADR + middleware 註解)
|
||
- 相關 review:`.autoflow/05-implementation/review/oidc-G5-OB1-OB6-review.md` Major-3
|