153 lines
4.5 KiB
Go
153 lines
4.5 KiB
Go
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"
|
|
}
|
|
}
|