fix: close auth, permission, contract and e2e review blockers
This commit is contained in:
@@ -122,7 +122,7 @@ type LoginResponse struct {
|
||||
ExpiresIn int64 `json:"expires_in,omitempty"`
|
||||
User *UserInfo `json:"user,omitempty"`
|
||||
// RequiresTOTP 指示登录需要额外的TOTP验证(当设备未信任时)
|
||||
RequiresTOTP bool `json:"requires_totp,omitempty"`
|
||||
RequiresTOTP bool `json:"requires_totp,omitempty"`
|
||||
// TempToken 临时令牌,用于TOTP验证阶段(短生命周期,不可用于常规API)
|
||||
TempToken string `json:"temp_token,omitempty"`
|
||||
// UserID 当RequiresTOTP为true时返回,用于后续TOTP验证
|
||||
@@ -759,11 +759,16 @@ func (s *AuthService) Login(ctx context.Context, req *LoginRequest, ip string) (
|
||||
|
||||
// P0-07 安全修复:检查是否需要TOTP验证(用户启用了TOTP且设备未信任)
|
||||
if s.isTOTPRequiredForLogin(ctx, user, req.DeviceID) {
|
||||
tempToken, err := s.jwtManager.GenerateTOTPChallengeToken(user.ID, user.Username, req.DeviceID, user.PasswordChangedAt.Unix())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// 返回RequiresTOTP指示前端需要完成TOTP验证
|
||||
// 前端应调用 /auth/login/totp-verify 接口完成验证
|
||||
return &LoginResponse{
|
||||
RequiresTOTP: true,
|
||||
UserID: user.ID,
|
||||
TempToken: tempToken,
|
||||
UserID: user.ID,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -808,10 +813,27 @@ func (s *AuthService) isTOTPRequiredForLogin(ctx context.Context, user *domain.U
|
||||
// VerifyTOTPAfterPasswordLogin 完成密码登录后的TOTP验证
|
||||
// 当用户启用了TOTP但设备未信任时,密码登录会返回RequiresTOTP=true
|
||||
// 前端需要调用此接口完成TOTP验证以获取令牌
|
||||
func (s *AuthService) VerifyTOTPAfterPasswordLogin(ctx context.Context, userID int64, totpCode, deviceID string) (*LoginResponse, error) {
|
||||
func (s *AuthService) VerifyTOTPAfterPasswordLogin(ctx context.Context, userID int64, totpCode, deviceID, tempToken string) (*LoginResponse, error) {
|
||||
if s == nil {
|
||||
return nil, errors.New("auth service is not initialized")
|
||||
}
|
||||
if s.jwtManager == nil {
|
||||
return nil, errors.New("jwt manager is not configured")
|
||||
}
|
||||
|
||||
claims, err := s.jwtManager.ValidateTOTPChallengeToken(strings.TrimSpace(tempToken))
|
||||
if err != nil {
|
||||
return nil, errors.New("TOTP challenge is invalid or expired")
|
||||
}
|
||||
if claims == nil || claims.UserID != userID {
|
||||
return nil, errors.New("TOTP challenge does not match user")
|
||||
}
|
||||
if strings.TrimSpace(claims.DeviceID) != strings.TrimSpace(deviceID) {
|
||||
return nil, errors.New("TOTP challenge does not match device")
|
||||
}
|
||||
if s.IsTokenBlacklisted(ctx, claims.JTI) {
|
||||
return nil, errors.New("TOTP challenge has already been used")
|
||||
}
|
||||
|
||||
user, err := s.userRepo.GetByID(ctx, userID)
|
||||
if err != nil {
|
||||
@@ -826,6 +848,9 @@ func (s *AuthService) VerifyTOTPAfterPasswordLogin(ctx context.Context, userID i
|
||||
if err := s.VerifyTOTP(ctx, userID, totpCode, deviceID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := s.blacklistTokenClaims(ctx, tempToken, s.jwtManager.ValidateTOTPChallengeToken); err != nil {
|
||||
return nil, fmt.Errorf("totp challenge revocation failed: %w", err)
|
||||
}
|
||||
|
||||
// TOTP验证成功,返回完整登录响应
|
||||
return s.generateLoginResponseWithoutRemember(ctx, user)
|
||||
@@ -902,18 +927,22 @@ func (s *AuthService) Logout(ctx context.Context, username string, req *LogoutRe
|
||||
return nil
|
||||
}
|
||||
|
||||
_ = s.blacklistTokenClaims(ctx, req.AccessToken, func(token string) (*auth.Claims, error) {
|
||||
if err := s.blacklistTokenClaims(ctx, req.AccessToken, func(token string) (*auth.Claims, error) {
|
||||
if s.jwtManager == nil {
|
||||
return nil, nil
|
||||
}
|
||||
return s.jwtManager.ValidateAccessToken(token)
|
||||
})
|
||||
_ = s.blacklistTokenClaims(ctx, req.RefreshToken, func(token string) (*auth.Claims, error) {
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.blacklistTokenClaims(ctx, req.RefreshToken, func(token string) (*auth.Claims, error) {
|
||||
if s.jwtManager == nil {
|
||||
return nil, nil
|
||||
}
|
||||
return s.jwtManager.ValidateRefreshToken(token)
|
||||
})
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if strings.TrimSpace(username) != "" {
|
||||
s.publishEvent(ctx, domain.EventUserLogout, map[string]interface{}{
|
||||
|
||||
Reference in New Issue
Block a user