veza/veza-backend-api/internal/handlers/two_factor_handler.go
senke 24b29d229d fix(v0.12.6.1): remediate 2 CRITICAL + 10 HIGH + 1 MEDIUM pentest findings
Security fixes implemented:

CRITICAL:
- CRIT-001: IDOR on chat rooms — added IsRoomMember check before
  returning room data or message history (returns 404, not 403)
- CRIT-002: play_count/like_count exposed publicly — changed JSON
  tags to "-" so they are never serialized in API responses

HIGH:
- HIGH-001: TOCTOU race on marketplace downloads — transaction +
  SELECT FOR UPDATE on GetDownloadURL
- HIGH-002: HS256 in production docker-compose — replaced JWT_SECRET
  with JWT_PRIVATE_KEY_PATH / JWT_PUBLIC_KEY_PATH (RS256)
- HIGH-003: context.Background() bypass in user repository — full
  context propagation from handlers → services → repository (29 files)
- HIGH-004: Race condition on promo codes — SELECT FOR UPDATE
- HIGH-005: Race condition on exclusive licenses — SELECT FOR UPDATE
- HIGH-006: Rate limiter IP spoofing — SetTrustedProxies(nil) default
- HIGH-007: RGPD hard delete incomplete — added cleanup for sessions,
  settings, follows, notifications, audit_logs anonymization
- HIGH-008: RTMP callback auth weak — fail-closed when unconfigured,
  header-only (no query param), constant-time compare
- HIGH-009: Co-listening host hijack — UpdateHostState now takes *Conn
  and verifies IsHost before processing
- HIGH-010: Moderator self-strike — added issuedBy != userID check

MEDIUM:
- MEDIUM-001: Recovery codes used math/rand — replaced with crypto/rand
- MEDIUM-005: Stream token forgeable — resolved by HIGH-002 (RS256)

Updated REMEDIATION_MATRIX: 14 findings marked  CORRIGÉ.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 05:40:53 +01:00

284 lines
10 KiB
Go

package handlers
import (
"context"
"net/http"
"veza-backend-api/internal/models"
"veza-backend-api/internal/services"
apperrors "veza-backend-api/internal/errors"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"go.uber.org/zap"
"golang.org/x/crypto/bcrypt"
)
// TwoFactorServiceInterface defines methods needed for 2FA handler
type TwoFactorServiceInterface interface {
GetTwoFactorStatus(ctx context.Context, userID uuid.UUID) (bool, error)
GenerateSecret(user *models.User) (*services.TwoFactorSetup, error)
VerifyTOTPCode(secret, code string) bool
GenerateRecoveryCodes() []string
EnableTwoFactor(ctx context.Context, userID uuid.UUID, secret string, recoveryCodes []string) error
DisableTwoFactor(ctx context.Context, userID uuid.UUID) error
}
// UserServiceInterface defines methods needed for user operations
type UserServiceInterface interface {
GetByID(ctx context.Context, userID uuid.UUID) (*models.User, error)
}
// TwoFactorHandler handles 2FA-related API endpoints
// BE-API-001: Implement 2FA endpoints (setup, verify, disable)
type TwoFactorHandler struct {
twoFactorService TwoFactorServiceInterface
userService UserServiceInterface
logger *zap.Logger
}
// NewTwoFactorHandler creates new 2FA handler
func NewTwoFactorHandler(twoFactorService *services.TwoFactorService, userService *services.UserService, logger *zap.Logger) *TwoFactorHandler {
return &TwoFactorHandler{
twoFactorService: twoFactorService,
userService: userService,
logger: logger,
}
}
// NewTwoFactorHandlerWithInterface creates new 2FA handler with interfaces (for testing)
func NewTwoFactorHandlerWithInterface(twoFactorService TwoFactorServiceInterface, userService UserServiceInterface, logger *zap.Logger) *TwoFactorHandler {
return &TwoFactorHandler{
twoFactorService: twoFactorService,
userService: userService,
logger: logger,
}
}
// SetupTwoFactorRequest represents the request for 2FA setup
type SetupTwoFactorRequest struct {
// No fields needed - user is authenticated
}
// SetupTwoFactorResponse represents the response for 2FA setup
type SetupTwoFactorResponse struct {
Secret string `json:"secret"`
QRCodeURL string `json:"qr_code_url"`
RecoveryCodes []string `json:"recovery_codes"`
}
// SetupTwoFactor initiates 2FA setup for a user
// @Summary Setup 2FA
// @Description Generate 2FA secret and QR code for setup
// @Tags Auth
// @Accept json
// @Produce json
// @Security BearerAuth
// @Success 200 {object} APIResponse{data=SetupTwoFactorResponse}
// @Failure 400 {object} APIResponse "2FA already enabled"
// @Failure 401 {object} APIResponse "Unauthorized"
// @Failure 500 {object} APIResponse "Internal Error"
// @Router /auth/2fa/setup [post]
func (h *TwoFactorHandler) SetupTwoFactor(c *gin.Context) {
userID, ok := GetUserIDUUID(c)
if !ok {
return // Error already sent by GetUserIDUUID
}
// Check if 2FA is already enabled
enabled, err := h.twoFactorService.GetTwoFactorStatus(c.Request.Context(), userID)
if err != nil {
h.logger.Error("Failed to get 2FA status", zap.Error(err), zap.String("user_id", userID.String()))
RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Failed to get 2FA status", err))
return
}
if enabled {
RespondWithAppError(c, apperrors.New(apperrors.ErrCodeValidation, "2FA is already enabled"))
return
}
// Get user information
user, err := h.userService.GetByID(c.Request.Context(), userID)
if err != nil {
h.logger.Error("Failed to get user", zap.Error(err), zap.String("user_id", userID.String()))
RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Failed to get user information", err))
return
}
// Generate 2FA setup
setup, err := h.twoFactorService.GenerateSecret(user)
if err != nil {
h.logger.Error("Failed to generate 2FA setup", zap.Error(err), zap.String("user_id", userID.String()))
RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Failed to generate 2FA setup", err))
return
}
RespondSuccess(c, http.StatusOK, SetupTwoFactorResponse{
Secret: setup.Secret,
QRCodeURL: setup.QRCodeURL,
RecoveryCodes: setup.RecoveryCodes,
})
}
// VerifyTwoFactorRequest represents the request for 2FA verification
type VerifyTwoFactorRequest struct {
Secret string `json:"secret" binding:"required"` // Secret from setup step
Code string `json:"code" binding:"required"` // TOTP code to verify
}
// VerifyTwoFactor verifies a 2FA code and enables 2FA
// @Summary Verify and Enable 2FA
// @Description Verify 2FA code and enable 2FA for user
// @Tags Auth
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param request body VerifyTwoFactorRequest true "2FA Code"
// @Success 200 {object} APIResponse{data=object{message=string}}
// @Failure 400 {object} APIResponse "Invalid code"
// @Failure 401 {object} APIResponse "Unauthorized"
// @Failure 500 {object} APIResponse "Internal Error"
// @Router /auth/2fa/verify [post]
func (h *TwoFactorHandler) VerifyTwoFactor(c *gin.Context) {
userID, ok := GetUserIDUUID(c)
if !ok {
return // Error already sent by GetUserIDUUID
}
var req VerifyTwoFactorRequest
commonHandler := NewCommonHandler(h.logger)
if appErr := commonHandler.BindAndValidateJSON(c, &req); appErr != nil {
RespondWithAppError(c, appErr)
return
}
// Check if 2FA is already enabled
enabled, err := h.twoFactorService.GetTwoFactorStatus(c.Request.Context(), userID)
if err != nil {
h.logger.Error("Failed to get 2FA status", zap.Error(err), zap.String("user_id", userID.String()))
RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Failed to get 2FA status", err))
return
}
if enabled {
RespondWithAppError(c, apperrors.New(apperrors.ErrCodeValidation, "2FA is already enabled"))
return
}
// Verify the code with the secret provided from setup step
valid := h.twoFactorService.VerifyTOTPCode(req.Secret, req.Code)
if !valid {
RespondWithAppError(c, apperrors.New(apperrors.ErrCodeValidation, "Invalid 2FA code"))
return
}
// Generate recovery codes for this secret
recoveryCodes := h.twoFactorService.GenerateRecoveryCodes()
// Enable 2FA with the verified secret
err = h.twoFactorService.EnableTwoFactor(c.Request.Context(), userID, req.Secret, recoveryCodes)
if err != nil {
h.logger.Error("Failed to enable 2FA", zap.Error(err), zap.String("user_id", userID.String()))
RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Failed to enable 2FA", err))
return
}
RespondSuccess(c, http.StatusOK, gin.H{"message": "2FA enabled successfully"})
}
// DisableTwoFactorRequest represents the request for disabling 2FA
type DisableTwoFactorRequest struct {
Password string `json:"password" binding:"required" validate:"required"`
}
// DisableTwoFactor disables 2FA for a user (requires password confirmation)
// @Summary Disable 2FA
// @Description Disable 2FA for user (requires password confirmation)
// @Tags Auth
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param request body DisableTwoFactorRequest true "Password Confirmation"
// @Success 200 {object} APIResponse{data=object{message=string}}
// @Failure 400 {object} APIResponse "Invalid password or 2FA not enabled"
// @Failure 401 {object} APIResponse "Unauthorized"
// @Failure 500 {object} APIResponse "Internal Error"
// @Router /auth/2fa/disable [post]
func (h *TwoFactorHandler) DisableTwoFactor(c *gin.Context) {
userID, ok := GetUserIDUUID(c)
if !ok {
return // Error already sent by GetUserIDUUID
}
var req DisableTwoFactorRequest
commonHandler := NewCommonHandler(h.logger)
if appErr := commonHandler.BindAndValidateJSON(c, &req); appErr != nil {
RespondWithAppError(c, appErr)
return
}
// Check if 2FA is enabled
enabled, err := h.twoFactorService.GetTwoFactorStatus(c.Request.Context(), userID)
if err != nil {
h.logger.Error("Failed to get 2FA status", zap.Error(err), zap.String("user_id", userID.String()))
RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Failed to get 2FA status", err))
return
}
if !enabled {
RespondWithAppError(c, apperrors.New(apperrors.ErrCodeValidation, "2FA is not enabled"))
return
}
// SEC-001: Verify password before disabling 2FA
user, err := h.userService.GetByID(c.Request.Context(), userID)
if err != nil {
h.logger.Error("Failed to get user", zap.Error(err), zap.String("user_id", userID.String()))
RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Failed to get user", err))
return
}
if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(req.Password)); err != nil {
h.logger.Warn("2FA disable failed: invalid password", zap.String("user_id", userID.String()))
RespondWithAppError(c, apperrors.New(apperrors.ErrCodeValidation, "Invalid password"))
return
}
// Disable 2FA
err = h.twoFactorService.DisableTwoFactor(c.Request.Context(), userID)
if err != nil {
h.logger.Error("Failed to disable 2FA", zap.Error(err), zap.String("user_id", userID.String()))
RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Failed to disable 2FA", err))
return
}
RespondSuccess(c, http.StatusOK, gin.H{"message": "2FA disabled successfully"})
}
// GetTwoFactorStatus gets the 2FA status for a user
// @Summary Get 2FA Status
// @Description Get 2FA enabled status for authenticated user
// @Tags Auth
// @Accept json
// @Produce json
// @Security BearerAuth
// @Success 200 {object} APIResponse{data=object{enabled=bool}}
// @Failure 401 {object} APIResponse "Unauthorized"
// @Failure 500 {object} APIResponse "Internal Error"
// @Router /auth/2fa/status [get]
func (h *TwoFactorHandler) GetTwoFactorStatus(c *gin.Context) {
userID, ok := GetUserIDUUID(c)
if !ok {
return // Error already sent by GetUserIDUUID
}
enabled, err := h.twoFactorService.GetTwoFactorStatus(c.Request.Context(), userID)
if err != nil {
h.logger.Error("Failed to get 2FA status", zap.Error(err), zap.String("user_id", userID.String()))
RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Failed to get 2FA status", err))
return
}
RespondSuccess(c, http.StatusOK, gin.H{"enabled": enabled})
}