// Package auth 定義 visionA-backend 的雙層 Auth 介面(AuthService / AuthProvider) // 以及 Pairing / Session Token 的型別與 Store。 // // 介面設計對齊 TDD §2.2、security.md §2.0 與 PRD interface-contracts.md §8.2。 // // 從 Phase 0.6(OB5)起,唯一的認證路徑是 OIDC + cookie session(見 internal/oidc/ // 與 internal/usersession/),因此本 package 不再提供 AuthProvider / AuthService 的 // 內建實作。介面本身仍保留,供未來新增備援 provider(例:Phase 1 接 backup local auth、 // service-to-service token)時直接套用,不必重新發明 contract。 // // 本 package 仍負責提供: // - UserContext 等領域型別 // - PairingToken / SessionToken 結構與其 Store 介面 // - PairingStore / SessionTokenStore 的 in-memory 實作 package auth import ( "context" "errors" "net/http" "time" ) // ========================================================================== // 錯誤型別(公開 sentinel errors,便於 caller 用 errors.Is 比對) // ========================================================================== var ( // ErrNotImplemented 用於雛形 stub,表示此功能 Phase 0 尚未實作。 ErrNotImplemented = errors.New("auth: not implemented in phase 0") // ErrInvalidToken 表示 token 格式錯誤、過期、或不被此 provider 認識。 ErrInvalidToken = errors.New("auth: invalid token") // ErrTokenExpired 表示 token 過了 ExpiresAt。 ErrTokenExpired = errors.New("auth: token expired") // ErrTokenUsed 表示一次性 token(pairing)已經被消費。 ErrTokenUsed = errors.New("auth: token already used") // ErrTokenRevoked 表示 token 已被使用者或系統撤銷。 ErrTokenRevoked = errors.New("auth: token revoked") // ErrInvalidCredentials 表示 email / password 比對失敗(Phase 1 實作)。 ErrInvalidCredentials = errors.New("auth: invalid credentials") // ErrUserNotFound 表示查詢的 user 不存在。 ErrUserNotFound = errors.New("auth: user not found") // ErrUserAlreadyExists 表示註冊時 email 已存在(Phase 1 實作)。 ErrUserAlreadyExists = errors.New("auth: user already exists") ) // ========================================================================== // Domain types // ========================================================================== // User 是 Auth 系統觀察到的使用者。雛形固定為 demo-user;Phase 1 對接真實 DB。 // // 註:完整 User struct 定義於 database.md §2.1;這裡保留 auth 層所需欄位即可。 type User struct { ID string `json:"id"` Email string `json:"email"` Name string `json:"name,omitempty"` CreatedAt time.Time `json:"createdAt"` UpdatedAt time.Time `json:"updatedAt"` } // UserContext 是從 request 解析出來、後續 handler 可信賴的使用者身分資訊。 // // Middleware 層(AuthService.Authenticate)負責產生此 context; // Handler 不需知道 token 從哪來,只需讀 UserContext。 type UserContext struct { UserID string `json:"userId"` Email string `json:"email,omitempty"` Roles []string `json:"roles,omitempty"` OrgID string `json:"orgId,omitempty"` } // LoginRequest 是 Login 的輸入參數。 type LoginRequest struct { Email string `json:"email"` Password string `json:"password"` } // LoginResult 是 Login 成功後回傳的完整資訊。 type LoginResult struct { User *User `json:"user"` AccessToken string `json:"accessToken"` RefreshToken string `json:"refreshToken,omitempty"` ExpiresAt time.Time `json:"expiresAt"` } // RegisterRequest 是 Register 的輸入參數。 type RegisterRequest struct { Email string `json:"email"` Password string `json:"password"` Name string `json:"name,omitempty"` } // ========================================================================== // Token types // ========================================================================== // TokenKind 區分 token 的生命週期類型。 type TokenKind string const ( // KindPairing 是短期一次性 token(15 min TTL),用於首次 agent 配對。 KindPairing TokenKind = "pairing" // KindSession 是長期可撤銷 token(90 天 TTL),agent 升級後使用。 KindSession TokenKind = "session" ) // PairingToken 代表一個已發行(尚未消費)的 pairing token 紀錄。 // // 格式:vAc_ + 32 hex(共 36 字元);見 security.md §1.3。 // DB 僅存 TokenHash(sha256 plaintext),原文 token 僅在建立時回傳一次。 // // 雛形 InMemoryPairingStore 存的是明文 token 作為 key,Phase 1 改為 hash。 type PairingToken struct { // Plaintext 是原文 token(僅在建立時回傳給 caller;儲存層請改存 hash)。 Plaintext string `json:"-"` // TokenHash 是 sha256(Plaintext) 的 hex 表示,DB 實際 PK。 TokenHash string `json:"-"` UserID string `json:"userId"` DeviceID string `json:"deviceId,omitempty"` Kind TokenKind `json:"kind"` CreatedAt time.Time `json:"createdAt"` ExpiresAt *time.Time `json:"expiresAt,omitempty"` UsedAt *time.Time `json:"usedAt,omitempty"` RevokedAt *time.Time `json:"revokedAt,omitempty"` } // IsExpired 回報此 token 是否已過 ExpiresAt。 // ExpiresAt 為 nil 代表永不過期(僅 Phase 1 的 session token 可能如此設定)。 func (t *PairingToken) IsExpired(now time.Time) bool { if t.ExpiresAt == nil { return false } return now.After(*t.ExpiresAt) } // IsUsed 回報此 token 是否已被消費(一次性 pairing token 用)。 func (t *PairingToken) IsUsed() bool { return t.UsedAt != nil } // IsRevoked 回報此 token 是否已撤銷。 func (t *PairingToken) IsRevoked() bool { return t.RevokedAt != nil } // SessionToken 代表升級後的長期 tunnel session token(Phase 1 使用)。 // // 格式:vAs_ + 64 hex(共 68 字元);見 security.md §1.3。 // 雛形階段以單階段 pairing token 代替,故 SessionToken struct 目前主要作為型別佔位。 type SessionToken struct { Plaintext string `json:"-"` TokenHash string `json:"-"` UserID string `json:"userId"` DeviceID string `json:"deviceId"` ParentTokenHash string `json:"-"` CreatedAt time.Time `json:"createdAt"` ExpiresAt *time.Time `json:"expiresAt,omitempty"` RevokedAt *time.Time `json:"revokedAt,omitempty"` } // ========================================================================== // Interfaces // ========================================================================== // AuthService 是 middleware 層介面:每個 HTTP request 進來時由它解析身分。 // // 實作時通常 wrap 一個 AuthProvider 或直接讀 cookie / header。 // 雛形 StaticAuthService 永遠回 demo-user,方便前端開發時不需要真正登入。 type AuthService interface { // Authenticate 從 HTTP request 解析出 UserContext。 // 若無法認證,回傳具體錯誤(例:ErrInvalidToken);middleware 應將其轉為 401。 Authenticate(ctx context.Context, r *http.Request) (*UserContext, error) // Authorize 判斷此 UserContext 是否有權對 resource 做 action。 // 雛形回 nil(全放);Phase 1 以 RBAC 實作。 Authorize(ctx context.Context, uc *UserContext, resource, action string) error } // AuthProvider 是 handler 層介面:處理登入 / 註冊 / 登出 / token 驗證等明確動作。 // // 此介面對齊 PRD interface-contracts.md §8.2。雛形以 StaticAuthProvider 填入, // Phase 1 換為 JWTAuthProvider(綁 DB + JWT 簽章)不影響呼叫端。 type AuthProvider interface { Register(ctx context.Context, req *RegisterRequest) (*User, error) Login(ctx context.Context, req *LoginRequest) (*LoginResult, error) Logout(ctx context.Context, token string) error ValidateToken(ctx context.Context, token string) (*UserContext, error) GetUser(ctx context.Context, userID string) (*User, error) } // PairingStore 管理 pairing token 的生命週期。 // // Phase 0 使用 InMemoryPairingStore(map + mutex + TTL 清理); // Phase 1 改為 PostgresPairingStore 並加入兩階段升級(pairing → session)。 // // 注意一次性使用的語意:MarkUsed 後 Validate 必須失敗。 type PairingStore interface { // Create 產生並保存一個新的 pairing token。 // plaintext 為原文 token(caller 只此一次能拿到),info 為儲存層表示,err 為產生錯誤。 Create(ctx context.Context, userID string, ttl time.Duration) (plaintext string, info *PairingToken, err error) // Validate 檢查 token 是否有效(存在、未過期、未被使用、未被撤銷)。 // 驗證通過回傳 token 資訊,否則回具體錯誤(ErrInvalidToken / ErrTokenExpired / ...)。 Validate(ctx context.Context, token string) (*PairingToken, error) // MarkUsed 標記一次性 token 為已使用;之後 Validate 必須失敗。 MarkUsed(ctx context.Context, token string, deviceID string) error // Revoke 撤銷一個 token(使用者操作或系統判定)。 Revoke(ctx context.Context, token string) error // List 列出某使用者的所有 pairing token(含已使用 / 已撤銷,供 UI 顯示)。 List(ctx context.Context, userID string) ([]*PairingToken, error) // CleanupExpired 清除超過 ExpiresAt 的 token; // 由 background goroutine 週期性呼叫(建議每分鐘)。 // 回傳被移除的數量,便於觀測。 CleanupExpired(ctx context.Context, now time.Time) (removed int, err error) }