package useCases import ( "RenewCMS/internal/domain/audit" "RenewCMS/internal/domain/hash" "RenewCMS/internal/domain/user" "context" "crypto/rand" "errors" "sync" ) type AuthenticateUserUseCase struct { userRepository user.Repository hasher hash.Hasher recorder audit.Recorder decoyOnce sync.Once decoyHash string } func NewAuthenticateUserUseCase(userRepository user.Repository, hasher hash.Hasher, recorder audit.Recorder) *AuthenticateUserUseCase { return &AuthenticateUserUseCase{userRepository: userRepository, hasher: hasher, recorder: recorder} } func (a *AuthenticateUserUseCase) Authenticate(ctx context.Context, username string, password string) (user.User, error) { dbUser, err := a.verifyCredentials(username, password) if err != nil { a.recordRejectedLogin(ctx, username, err) return user.User{}, err } event := audit.NewEvent(audit.ActionLogin, audit.CategoryAuth) event.Actor = audit.UserActor(dbUser) a.recorder.Record(ctx, event) return dbUser, nil } func (a *AuthenticateUserUseCase) verifyCredentials(username string, password string) (user.User, error) { dbUser, err := a.userRepository.GetByUsername(username) if err != nil { if errors.Is(err, user.ErrUserNotFound) { a.compareWithDecoy(password) } return user.User{}, err } if err := a.hasher.Compare(dbUser.Password, password); err != nil { return user.User{}, user.ErrInvalidCredentials } return dbUser, nil } func (a *AuthenticateUserUseCase) compareWithDecoy(password string) { a.decoyOnce.Do(func() { a.decoyHash, _ = a.hasher.Hash(rand.Text()) }) if a.decoyHash != "" { _ = a.hasher.Compare(a.decoyHash, password) } } func (a *AuthenticateUserUseCase) recordRejectedLogin(ctx context.Context, username string, cause error) { if !errors.Is(cause, user.ErrUserNotFound) && !errors.Is(cause, user.ErrInvalidCredentials) { return } reason := audit.ReasonWrongLogin event := audit.NewEvent(audit.ActionLoginFailed, audit.CategoryAuth) event.Actor = audit.AnonymousActor(username) event.Outcome = audit.OutcomeFailed event.FailureReason = &reason event.Security = true a.recorder.Record(ctx, event) } func (a *AuthenticateUserUseCase) Logout(ctx context.Context) { origin, ok := audit.OriginFrom(ctx) if !ok || origin.Actor.Type != audit.ActorUser { return } a.recorder.Record(ctx, audit.NewEvent(audit.ActionLogout, audit.CategoryAuth)) }