從 edge-ai-platform POC 轉為正式產品的雲端後端,含以下整合階段:
- Phase 0:雛形骨架 — `cmd/api-server` (REST :3721) + `cmd/remote-proxy`
(tunnel :3800 / internal :3801) 雙 binary 共用 internal/,沿用 POC 的
WebSocket+yamux tunnel 協定但解耦 relay 與 API
- Phase 0.6:OIDC BFF 接 Innovedus Member Center
- internal/oidc package(coreos/go-oidc + PKCE S256 + state + nonce)
- internal/usersession package(HMAC-SHA256 cookie + RotateSessionID
防 session fixation, OWASP ASVS V3.2.1)
- 4 個 OIDC handler(/api/auth/login|callback|me|logout)+ AuthMiddleware
- 完全拔除 StaticAuthProvider,OIDC 是唯一認證路徑
- 9 個 ADR(含 ADR-010 BFF / ADR-011 取代 static auth /
ADR-012 pending session shared cookie / ADR-013 PKCE-only public client)
- Phase 0.7:A1 改造 + security audit 修復
- OIDC ClientSecret 變選填,支援 stage MC 的 public PKCE-only client
(AuthStyleInParams 強制 token endpoint 不送 client_secret)
- 預留 ServiceClient* 欄位給未來 client_credentials grant
- 移除 13+ 處 resolveUserID(uc, StaticUserID) fallback 改 strict mode
(Audit C1:multi-tenant 隔離破口)
- Pairing exchange MarkUsed 失敗 abort + revoke session token(Audit M3)
- 新增 all_endpoints_require_auth_test 整合測試(51 endpoint × 401)
驗證:go test -race -count=3 ./... 17 packages 全綠 / go vet 0 warning
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
123 lines
5.5 KiB
Go
123 lines
5.5 KiB
Go
// 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)
|
||
}
|