From 4acd19f420b4f5244e472d09a297c1ea102d1ef3 Mon Sep 17 00:00:00 2001 From: long-agent Date: Sat, 18 Apr 2026 14:19:15 +0800 Subject: [PATCH] fix: P0-07 prevent login bypassing TOTP verification - Add RequiresTOTP, TempToken, UserID fields to LoginResponse - Add isTOTPRequiredForLogin() to check if TOTP is needed after password - Add VerifyTOTPAfterPasswordLogin() for completing login with TOTP - Login() now checks if TOTP is required after password verification When user has TOTP enabled and device is not trusted: - Login returns {requires_totp: true, user_id: } instead of token - Frontend should prompt for TOTP code - Frontend calls VerifyTOTPAfterPasswordLogin to complete login Note: Frontend changes are required to handle the new login flow. The TempToken field is reserved for future use. --- internal/service/auth.go | 73 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 69 insertions(+), 4 deletions(-) diff --git a/internal/service/auth.go b/internal/service/auth.go index 90c426b..4f3c387 100644 --- a/internal/service/auth.go +++ b/internal/service/auth.go @@ -117,10 +117,16 @@ type UserInfo struct { } type LoginResponse struct { - AccessToken string `json:"access_token"` - RefreshToken string `json:"refresh_token"` - ExpiresIn int64 `json:"expires_in"` - User *UserInfo `json:"user"` + AccessToken string `json:"access_token,omitempty"` + RefreshToken string `json:"refresh_token,omitempty"` + ExpiresIn int64 `json:"expires_in,omitempty"` + User *UserInfo `json:"user,omitempty"` + // RequiresTOTP 指示登录需要额外的TOTP验证(当设备未信任时) + RequiresTOTP bool `json:"requires_totp,omitempty"` + // TempToken 临时令牌,用于TOTP验证阶段(短生命周期,不可用于常规API) + TempToken string `json:"temp_token,omitempty"` + // UserID 当RequiresTOTP为true时返回,用于后续TOTP验证 + UserID int64 `json:"user_id,omitempty"` } type LogoutRequest struct { @@ -751,6 +757,16 @@ func (s *AuthService) Login(ctx context.Context, req *LoginRequest, ip string) ( _ = s.cache.Delete(ctx, attemptKey) } + // P0-07 安全修复:检查是否需要TOTP验证(用户启用了TOTP且设备未信任) + if s.isTOTPRequiredForLogin(ctx, user, req.DeviceID) { + // 返回RequiresTOTP指示前端需要完成TOTP验证 + // 前端应调用 /auth/login/totp-verify 接口完成验证 + return &LoginResponse{ + RequiresTOTP: true, + UserID: user.ID, + }, nil + } + s.bestEffortUpdateLastLogin(ctx, user.ID, ip, "password") s.cacheUserInfo(ctx, user) s.writeLoginLog(ctx, &user.ID, domain.LoginTypePassword, ip, true, "") @@ -766,6 +782,55 @@ func (s *AuthService) Login(ctx context.Context, req *LoginRequest, ip string) ( return s.generateLoginResponse(ctx, user, req.Remember) } +// isTOTPRequiredForLogin 检查登录是否需要TOTP验证 +// 条件:用户启用了TOTP且尝试登录的设备未信任 +func (s *AuthService) isTOTPRequiredForLogin(ctx context.Context, user *domain.User, deviceID string) bool { + if user == nil { + return false + } + // 检查用户是否启用了TOTP + if !user.TOTPEnabled || strings.TrimSpace(user.TOTPSecret) == "" { + return false + } + // 检查设备是否已信任 + if deviceID != "" && s.deviceService != nil { + device, err := s.deviceService.GetDeviceByDeviceID(ctx, user.ID, deviceID) + if err == nil && device.IsTrusted { + // 设备已信任,检查信任是否过期 + if device.TrustExpiresAt == nil || device.TrustExpiresAt.After(time.Now()) { + return false // 设备已信任且未过期,不需要TOTP + } + } + } + return true // 需要TOTP验证 +} + +// VerifyTOTPAfterPasswordLogin 完成密码登录后的TOTP验证 +// 当用户启用了TOTP但设备未信任时,密码登录会返回RequiresTOTP=true +// 前端需要调用此接口完成TOTP验证以获取令牌 +func (s *AuthService) VerifyTOTPAfterPasswordLogin(ctx context.Context, userID int64, totpCode, deviceID string) (*LoginResponse, error) { + if s == nil { + return nil, errors.New("auth service is not initialized") + } + + user, err := s.userRepo.GetByID(ctx, userID) + if err != nil { + return nil, errors.New("用户不存在") + } + + if err := s.ensureUserActive(user); err != nil { + return nil, err + } + + // 验证TOTP + if err := s.VerifyTOTP(ctx, userID, totpCode, deviceID); err != nil { + return nil, err + } + + // TOTP验证成功,返回完整登录响应 + return s.generateLoginResponseWithoutRemember(ctx, user) +} + func (s *AuthService) RefreshToken(ctx context.Context, refreshToken string) (*LoginResponse, error) { if s == nil || s.jwtManager == nil || s.userRepo == nil { return nil, errors.New("auth service is not fully configured")