// Package usersession 提供 BFF 模式下「browser ↔ api-server」的 HTTP user session 管理。 // // 與 internal/session(tunnel session)刻意分開命名以避免混淆: // - internal/session = api-server / remote-proxy 之間的 yamux tunnel session // - internal/usersession = browser cookie 對應的「使用者登入 session」 // // 設計對齊: // - oidc-tdd.md §4.3(internal/usersession/ 模組) // - oidc-tdd.md §5 (Session 設計、cookie、簽章) // - oidc-tdd.md §14 (安全考量:HttpOnly、SameSite、HMAC、不洩漏 token) // - adr-010-oidc-bff.md // // 此 package 只負責「session store + cookie」職責: // - Session 物件(含 OIDC pending state 與 token snapshot) // - Store interface + InMemoryStore(雛形實作,預留 Redis / DB) // - HMAC-SHA256 簽章 cookie 編解碼 // - Manager(把 Store 與 cookie 包成 handler-friendly helper) // // 不負責: // - OIDC 流程本身(由 internal/oidc 處理) // - HTTP middleware / handler(由 OB3 / OB4 處理) // - 與 internal/auth.AuthService 的整合(由 OB4 處理) package usersession import ( "context" "time" ) // Session 代表一個已建立的使用者 session。 // // 雛形階段為了實作簡單,把「OIDC login 流程暫存值」與「callback 後的 token snapshot」 // 放在同一個 struct 內: // - OIDCState / OIDCNonce / OIDCCodeVerifier 只在 /api/auth/login → /api/auth/callback // 之間使用,callback 完成後 caller 應將其清空。 // - AccessToken / IDTokenRaw 在 callback 完成後填入,雛形階段未必每個欄位都會用到 // (見 oidc-tdd.md §5、§14.3)。 // // 並發注意:Session pointer 由 Store 持有,caller 在 Update 之前修改自己的副本即可, // 不要在多 goroutine 間共享同一個 *Session 指標寫。 type Session struct { // ID 是隨機 32 bytes(base64url 編碼後 43 字元),由 Store.Create 產生。 // cookie 中存的是 ID + HMAC,server 端用 ID 對應到此 Session。 ID string // UserID 對應 OIDC id_token 的 sub claim。 // 雛形 pending session(OIDC dance 中、尚未 callback 完成)此欄位為空。 UserID string // Email / Name 從 OIDC id_token 的 email / name claim 取得。 Email string Name string // CreatedAt 是 Store.Create 的時間(用於 absoluteTimeout 比對)。 CreatedAt time.Time // LastSeenAt 由 Store.Update 自動更新(用於 idleTimeout 比對)。 LastSeenAt time.Time // ─── OIDC pending state(login → callback 暫存)─── // 這些欄位只在 OIDC dance 進行中需要。callback 處理完後 caller 應清空, // 避免長期存放於 in-memory store(即便 in-memory 風險低,仍是好習慣)。 // OIDCState 是 CSRF 防護 token,對應 authorize URL 的 state param。 OIDCState string // OIDCNonce 是 OIDC ID Token replay 防護,對應 authorize URL 的 nonce param。 OIDCNonce string // OIDCCodeVerifier 是 PKCE verifier(base64url 後 43 字元)。 // **絕對不可放進 cookie**(cookie 端只放 sessionID + HMAC),必須 server-side 持有。 OIDCCodeVerifier string // ─── OIDC token snapshot(callback 完成後填入)─── // AccessToken 是 OIDC access_token(雛形未必使用,預留給未來呼叫 MC API)。 // 不可進入任何 log(見 oidc-tdd.md §14.5)。 AccessToken string // IDTokenRaw 是 OIDC id_token raw 字串(驗簽通過後保留)。 // 用於除錯或未來 RP-initiated logout,雛形不必每次都存。 // 不可進入任何 log。 IDTokenRaw string // Extra 給 caller 放自訂 metadata(例如 return_to URL、debug flags)。 // 雛形 in-memory 直接共用 map;Phase 1 換 Redis 時須序列化(接 interface 的 caller 應自我約束放可序列化的型別)。 Extra map[string]any } // Store 是 user session 的儲存抽象。 // // 雛形:InMemoryStore(process-local map)。 // Phase 1:RedisStore / 或其他持久化實作(接同一個 interface)。 // // 所有方法應為 context-aware 與並發安全。 type Store interface { // Create 產生一個新的 Session(含隨機 ID、CreatedAt、LastSeenAt = now)並儲存。 // 回傳的 Session 為 store 持有的 pointer 的副本(修改後須呼叫 Update 才會持久化)。 Create(ctx context.Context) (*Session, error) // Get 依 ID 取出 Session。 // 找不到回 ErrNoSession。**不**自動更新 LastSeenAt(避免無條件刷新延長 idle window); // 由 Manager.GetSession / caller 決定何時 Update。 Get(ctx context.Context, id string) (*Session, error) // Update 將 caller 修改過的 Session 寫回 store。 // 自動將 LastSeenAt 更新為 now(同時 caller 對 Session 其他欄位的修改也會生效)。 // 找不到 ID 對應的 session 回 ErrNoSession。 Update(ctx context.Context, sess *Session) error // Delete 移除指定 ID 的 session(logout 用)。 // 不存在為 no-op,不回 error(與 internal/session.Store.Unregister 設計一致)。 Delete(ctx context.Context, id string) error // CleanupExpired 清掉所有逾時的 session。 // - idleTimeout:LastSeenAt 距今超過此時間 → 清除 // - absoluteTimeout:CreatedAt 距今超過此時間 → 清除(即使一直在用) // // 任一條件成立即清除。回傳清除數量供觀測使用。 // 由 caller(通常是 background goroutine,例如每 60s)週期呼叫。 CleanupExpired(ctx context.Context, idleTimeout, absoluteTimeout time.Duration) (removed int, err error) }