依 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)。
6.0 KiB
6.0 KiB
ADR-012:Pending Session 與 Logged-in Session 共用同一個 Cookie
狀態
Accepted — 2026-04-21
上位文件
背景 (Context)
OIDC Authorization Code + PKCE flow 在 backend 端會產生兩種 server-side session 狀態:
- Pending session(OIDC dance 進行中):
/api/auth/login階段建立,存放 PKCE code_verifier、CSRF state、OIDC nonce、return_to。生命週期短(≤ 10 分鐘,從使用者點 Login 到 IdP 完成同意),UserID 為空。 - 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 用)由以下三道防線阻擋:
-
AuthMiddleware 強制檢查
UserID == ""→ 401session_not_authenticatedinternal/api/middleware.goAuthMiddleware 在拿到 session 後立刻判斷 UserID 是否為空- 空 → 401(pending session 訪問 protected endpoint 一律拒絕)
- 此檢查不可拿掉、不可放寬
- 對應測試:
TestOIDCMiddleware_Rejects_PendingSession
-
Callback 完成時 rotate session ID(Fix-A1 / ADR-012 配套)
internal/api/oidc_auth.gocallback 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
-
Pending state 在 callback 完成同一次 UpdateSession 中清空
- OIDCState / OIDCNonce / OIDCCodeVerifier / Extra["return_to"] 在寫入 user info 的同一次 Update 中清掉
- 確保 logged-in session 不殘留 pending 欄位
理由 (Rationale)
為什麼維持合一
- Cookie 數量少對 frontend 簡單:frontend 只關心一個 cookie 是否存在,不需要區分 pending/logged-in 兩個 cookie 的狀態
- Handler 邏輯簡單:
oidcCallbackHandler不需要「讀 pending cookie → 建 new logged-in cookie → 清 pending cookie」三步操作;改 RotateSessionID 一步搞定 - Store 操作減少:每個 cookie 對應一個 store record,兩個 cookie 等於 store 寫入次數加倍
- 防護機制已到位:上述三道防線(middleware UserID 檢查 + RotateSessionID + 同一次清理)涵蓋原本兩個 cookie 設計要解的問題
為什麼不拆兩個 cookie
- 大改動:要動
usersession.Manager、oidc_auth.go兩處核心 handler、middleware、所有相關測試 - 不解新風險:pending vs logged-in 的混淆風險已由現有防線阻擋;拆兩個 cookie 只是把同一個保護換個位置實作
- Phase 1 Redis 化更複雜:兩個 cookie 對應兩個 store key,Redis 命名空間 / TTL 管理變複雜;合一設計直接共用一個 key
- 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 檢查
Sessionstruct 同時有 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;不影響架構 |