package gateway import ( "context" "errors" "go.uber.org/zap" "codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/pkg/logger" ) var ( ErrInvalidCredentials = errors.New("invalid email or password") ) // LoginInput represents the input for user login type LoginInput struct { Email string Password string } // LoginOutput represents the output after successful login type LoginOutput struct { UserID string UserEmail string UserName string UserRole string TenantID string } // LoginUseCase handles user authentication // Orchestrates the login workflow by coordinating focused usecases type LoginUseCase struct { // Focused usecases getUserByEmailUC *GetUserByEmailUseCase verifyPasswordUC *VerifyPasswordUseCase logger *zap.Logger } // NewLoginUseCase creates a new login use case func NewLoginUseCase( getUserByEmailUC *GetUserByEmailUseCase, verifyPasswordUC *VerifyPasswordUseCase, logger *zap.Logger, ) *LoginUseCase { return &LoginUseCase{ getUserByEmailUC: getUserByEmailUC, verifyPasswordUC: verifyPasswordUC, logger: logger.Named("login-usecase"), } } // ProvideLoginUseCase creates a new LoginUseCase for dependency injection func ProvideLoginUseCase( getUserByEmailUC *GetUserByEmailUseCase, verifyPasswordUC *VerifyPasswordUseCase, logger *zap.Logger, ) *LoginUseCase { return NewLoginUseCase(getUserByEmailUC, verifyPasswordUC, logger) } // Execute orchestrates the login workflow using focused usecases // CWE-208: Observable Timing Discrepancy - Uses timing-safe authentication func (uc *LoginUseCase) Execute(ctx context.Context, input *LoginInput) (*LoginOutput, error) { // CWE-532: Use hashed email to prevent PII in logs uc.logger.Info("authenticating user", logger.EmailHash(input.Email)) // Step 1: Get user by email globally (no tenant_id required for login) // Note: This returns ErrInvalidCredentials (not ErrUserNotFound) for security user, err := uc.getUserByEmailUC.Execute(ctx, input.Email) // CWE-208: TIMING ATTACK MITIGATION // We must ALWAYS verify the password, even if the user doesn't exist. // This prevents timing-based user enumeration attacks. // // Timing attack scenario without mitigation: // - If user exists: database lookup (~10ms) + Argon2 hashing (~100ms) = ~110ms // - If user doesn't exist: database lookup (~10ms) = ~10ms // Attacker can measure response time to enumerate valid email addresses. // // With mitigation: // - If user exists: database lookup + Argon2 hashing // - If user doesn't exist: database lookup + Argon2 dummy hashing // Both paths take approximately the same time (~110ms). var passwordHash string userExists := (err == nil && user != nil) if userExists { // User exists - use real password hash if user.SecurityData != nil { passwordHash = user.SecurityData.PasswordHash } } // If user doesn't exist, passwordHash remains empty string // The verifyPasswordUC will use dummy hash for timing safety // Step 2: Verify password - ALWAYS executed regardless of user existence if err := uc.verifyPasswordUC.ExecuteTimingSafe(input.Password, passwordHash, userExists); err != nil { // CWE-532: Use hashed email to prevent PII in logs if userExists { uc.logger.Warn("login failed: password verification failed", logger.EmailHash(input.Email), zap.String("tenant_id", user.TenantID)) } else { uc.logger.Warn("login failed: user not found", logger.EmailHash(input.Email)) } // Always return the same generic error regardless of reason return nil, ErrInvalidCredentials } // Now check if user lookup failed (after timing-safe password verification) if err != nil { // This should never happen because ExecuteTimingSafe should have failed // But keep for safety uc.logger.Error("unexpected error after password verification", zap.Error(err)) return nil, ErrInvalidCredentials } // CWE-532: Use hashed email to prevent PII in logs uc.logger.Info("user authenticated successfully", zap.String("user_id", user.ID), logger.EmailHash(user.Email), zap.String("tenant_id", user.TenantID)) // Convert role to string (1="executive", 2="manager", 3="staff") roleStr := getRoleString(user.Role) // Step 3: Build output return &LoginOutput{ UserID: user.ID, UserEmail: user.Email, UserName: user.Name, UserRole: roleStr, TenantID: user.TenantID, }, nil } // getRoleString converts numeric role to string representation func getRoleString(role int) string { switch role { case 1: return "executive" case 2: return "manager" case 3: return "staff" default: return "unknown" } }