jim800121chen 22f0837ba8 feat(visionA-backend): Phase 0 → 0.7 雲端後端(雙 binary + OIDC BFF + stage 部署)
從 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>
2026-05-01 11:21:20 +08:00

123 lines
5.5 KiB
Go
Raw Permalink 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.

// Package usersession 提供 BFF 模式下「browser ↔ api-server」的 HTTP user session 管理。
//
// 與 internal/sessiontunnel session刻意分開命名以避免混淆
// - internal/session = api-server / remote-proxy 之間的 yamux tunnel session
// - internal/usersession = browser cookie 對應的「使用者登入 session」
//
// 設計對齊:
// - oidc-tdd.md §4.3internal/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 bytesbase64url 編碼後 43 字元),由 Store.Create 產生。
// cookie 中存的是 ID + HMACserver 端用 ID 對應到此 Session。
ID string
// UserID 對應 OIDC id_token 的 sub claim。
// 雛形 pending sessionOIDC 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 statelogin → 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 verifierbase64url 後 43 字元)。
// **絕對不可放進 cookie**cookie 端只放 sessionID + HMAC必須 server-side 持有。
OIDCCodeVerifier string
// ─── OIDC token snapshotcallback 完成後填入)───
// 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 直接共用 mapPhase 1 換 Redis 時須序列化(接 interface 的 caller 應自我約束放可序列化的型別)。
Extra map[string]any
}
// Store 是 user session 的儲存抽象。
//
// 雛形InMemoryStoreprocess-local map
// Phase 1RedisStore / 或其他持久化實作(接同一個 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 的 sessionlogout 用)。
// 不存在為 no-op不回 error與 internal/session.Store.Unregister 設計一致)。
Delete(ctx context.Context, id string) error
// CleanupExpired 清掉所有逾時的 session。
// - idleTimeoutLastSeenAt 距今超過此時間 → 清除
// - absoluteTimeoutCreatedAt 距今超過此時間 → 清除(即使一直在用)
//
// 任一條件成立即清除。回傳清除數量供觀測使用。
// 由 caller通常是 background goroutine例如每 60s週期呼叫。
CleanupExpired(ctx context.Context, idleTimeout, absoluteTimeout time.Duration) (removed int, err error)
}