Initial commit: Open sourcing all of the Maple Open Technologies code.

This commit is contained in:
Bartlomiej Mika 2025-12-02 14:33:08 -05:00
commit 755d54a99d
2010 changed files with 448675 additions and 0 deletions

View file

@ -0,0 +1,153 @@
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"
}
}