Initial commit: Open sourcing all of the Maple Open Technologies code.
This commit is contained in:
commit
755d54a99d
2010 changed files with 448675 additions and 0 deletions
153
cloud/maplepress-backend/internal/usecase/gateway/login.go
Normal file
153
cloud/maplepress-backend/internal/usecase/gateway/login.go
Normal 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"
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue