-
This commit is contained in:
654
user/internal/services/auth_service.go
Normal file
654
user/internal/services/auth_service.go
Normal file
@ -0,0 +1,654 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"crypto/subtle"
|
||||
"encoding/base32"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/google/uuid"
|
||||
"github.com/pquerna/otp"
|
||||
"github.com/pquerna/otp/totp"
|
||||
"go.uber.org/zap"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
|
||||
"github.com/RyanCopley/skybridge/user/internal/domain"
|
||||
"github.com/RyanCopley/skybridge/user/internal/repository/interfaces"
|
||||
)
|
||||
|
||||
// AuthService defines the interface for authentication operations
|
||||
type AuthService interface {
|
||||
// Authentication
|
||||
Login(ctx context.Context, req *domain.LoginRequest, ipAddress, userAgent string) (*domain.LoginResponse, error)
|
||||
Register(ctx context.Context, req *domain.RegisterRequest, ipAddress, userAgent string) (*domain.RegisterResponse, error)
|
||||
Logout(ctx context.Context, token string) error
|
||||
RefreshToken(ctx context.Context, token string) (*domain.LoginResponse, error)
|
||||
|
||||
// Password management
|
||||
ForgotPassword(ctx context.Context, req *domain.ForgotPasswordRequest, ipAddress, userAgent string) error
|
||||
ResetPassword(ctx context.Context, req *domain.ResetPasswordRequest, ipAddress, userAgent string) error
|
||||
ChangePassword(ctx context.Context, userID uuid.UUID, req *domain.ChangePasswordRequest, actorID string) error
|
||||
|
||||
// Email verification
|
||||
VerifyEmail(ctx context.Context, req *domain.VerifyEmailRequest, ipAddress, userAgent string) error
|
||||
ResendVerification(ctx context.Context, req *domain.ResendVerificationRequest, ipAddress, userAgent string) error
|
||||
|
||||
// Two-factor authentication
|
||||
SetupTwoFactor(ctx context.Context, userID uuid.UUID) (*domain.SetupTwoFactorResponse, error)
|
||||
EnableTwoFactor(ctx context.Context, userID uuid.UUID, req *domain.EnableTwoFactorRequest, actorID string) error
|
||||
DisableTwoFactor(ctx context.Context, userID uuid.UUID, req *domain.DisableTwoFactorRequest, actorID string) error
|
||||
ValidateTwoFactor(ctx context.Context, req *domain.ValidateTwoFactorRequest, ipAddress, userAgent string) (*domain.LoginResponse, error)
|
||||
RegenerateTwoFactorBackupCodes(ctx context.Context, userID uuid.UUID, actorID string) ([]string, error)
|
||||
|
||||
// Session management
|
||||
GetUserSessions(ctx context.Context, userID uuid.UUID) (*domain.ListSessionsResponse, error)
|
||||
RevokeSession(ctx context.Context, userID uuid.UUID, sessionID uuid.UUID, actorID string) error
|
||||
RevokeAllSessions(ctx context.Context, userID uuid.UUID, actorID string) error
|
||||
|
||||
// Token validation
|
||||
ValidateToken(ctx context.Context, tokenString string) (*domain.AuthContext, error)
|
||||
|
||||
// Security utilities
|
||||
IsAccountLocked(ctx context.Context, email string) (bool, *time.Time, error)
|
||||
RecordLoginAttempt(ctx context.Context, email, ipAddress, userAgent, failureReason string, success bool) error
|
||||
}
|
||||
|
||||
type authService struct {
|
||||
userRepo interfaces.UserRepository
|
||||
sessionRepo interfaces.UserSessionRepository
|
||||
auditRepo interfaces.AuditRepository
|
||||
logger *zap.Logger
|
||||
jwtSecret string
|
||||
jwtIssuer string
|
||||
tokenExpiry time.Duration
|
||||
bcryptCost int
|
||||
maxLoginAttempts int
|
||||
lockoutDuration time.Duration
|
||||
}
|
||||
|
||||
// JWT Claims structure
|
||||
type JWTClaims struct {
|
||||
UserID string `json:"user_id"`
|
||||
Email string `json:"email"`
|
||||
Role string `json:"role"`
|
||||
SessionID string `json:"session_id"`
|
||||
TwoFactor bool `json:"two_factor,omitempty"`
|
||||
TempToken bool `json:"temp_token,omitempty"`
|
||||
jwt.RegisteredClaims
|
||||
}
|
||||
|
||||
// NewAuthService creates a new authentication service
|
||||
func NewAuthService(
|
||||
userRepo interfaces.UserRepository,
|
||||
sessionRepo interfaces.UserSessionRepository,
|
||||
auditRepo interfaces.AuditRepository,
|
||||
logger *zap.Logger,
|
||||
jwtSecret string,
|
||||
jwtIssuer string,
|
||||
) AuthService {
|
||||
return &authService{
|
||||
userRepo: userRepo,
|
||||
sessionRepo: sessionRepo,
|
||||
auditRepo: auditRepo,
|
||||
logger: logger,
|
||||
jwtSecret: jwtSecret,
|
||||
jwtIssuer: jwtIssuer,
|
||||
tokenExpiry: 24 * time.Hour,
|
||||
bcryptCost: 12,
|
||||
maxLoginAttempts: 5,
|
||||
lockoutDuration: 15 * time.Minute,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *authService) Register(ctx context.Context, req *domain.RegisterRequest, ipAddress, userAgent string) (*domain.RegisterResponse, error) {
|
||||
// Check if user already exists
|
||||
exists, err := s.userRepo.ExistsByEmail(ctx, req.Email)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to check user existence: %w", err)
|
||||
}
|
||||
if exists {
|
||||
return nil, fmt.Errorf("user with email %s already exists", req.Email)
|
||||
}
|
||||
|
||||
// Hash password
|
||||
passwordHash, err := s.hashPassword(req.Password)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to hash password: %w", err)
|
||||
}
|
||||
|
||||
// Generate email verification token
|
||||
verificationToken, err := s.generateSecureToken()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to generate verification token: %w", err)
|
||||
}
|
||||
|
||||
// Create user
|
||||
user := &domain.User{
|
||||
ID: uuid.New(),
|
||||
Email: req.Email,
|
||||
FirstName: req.FirstName,
|
||||
LastName: req.LastName,
|
||||
DisplayName: req.DisplayName,
|
||||
Role: domain.UserRoleUser,
|
||||
Status: domain.UserStatusPending,
|
||||
PasswordHash: passwordHash,
|
||||
EmailVerified: false,
|
||||
EmailVerificationToken: &verificationToken,
|
||||
EmailVerificationExpiresAt: timePtr(time.Now().Add(24 * time.Hour)),
|
||||
TwoFactorEnabled: false,
|
||||
CreatedBy: req.Email,
|
||||
UpdatedBy: req.Email,
|
||||
}
|
||||
|
||||
err = s.userRepo.Create(ctx, user)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create user: %w", err)
|
||||
}
|
||||
|
||||
// Log registration
|
||||
if s.auditRepo != nil {
|
||||
auditEvent := &interfaces.AuditEvent{
|
||||
ID: uuid.New(),
|
||||
Type: "auth.register",
|
||||
Severity: "info",
|
||||
Status: "success",
|
||||
Timestamp: time.Now().Format(time.RFC3339),
|
||||
ActorID: user.Email,
|
||||
ActorType: "user",
|
||||
ActorIP: ipAddress,
|
||||
UserAgent: userAgent,
|
||||
ResourceID: user.ID.String(),
|
||||
ResourceType: "user",
|
||||
Action: "register",
|
||||
Description: fmt.Sprintf("User %s registered", user.Email),
|
||||
Details: map[string]interface{}{
|
||||
"user_id": user.ID.String(),
|
||||
"email": user.Email,
|
||||
},
|
||||
}
|
||||
_ = s.auditRepo.LogEvent(ctx, auditEvent)
|
||||
}
|
||||
|
||||
// TODO: Send welcome email with verification link
|
||||
|
||||
return &domain.RegisterResponse{
|
||||
User: user,
|
||||
Message: "Registration successful. Please check your email for verification instructions.",
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *authService) Login(ctx context.Context, req *domain.LoginRequest, ipAddress, userAgent string) (*domain.LoginResponse, error) {
|
||||
// Check if account is locked
|
||||
locked, lockedUntil, err := s.IsAccountLocked(ctx, req.Email)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to check account lock status", zap.String("email", req.Email), zap.Error(err))
|
||||
return nil, fmt.Errorf("authentication failed")
|
||||
}
|
||||
|
||||
if locked {
|
||||
_ = s.RecordLoginAttempt(ctx, req.Email, ipAddress, userAgent, "account_locked", false)
|
||||
return nil, fmt.Errorf("account is temporarily locked until %v", lockedUntil.Format(time.RFC3339))
|
||||
}
|
||||
|
||||
// Get user
|
||||
user, err := s.userRepo.GetByEmail(ctx, req.Email)
|
||||
if err != nil {
|
||||
_ = s.RecordLoginAttempt(ctx, req.Email, ipAddress, userAgent, "user_not_found", false)
|
||||
return nil, fmt.Errorf("authentication failed")
|
||||
}
|
||||
|
||||
// Verify password
|
||||
if !s.verifyPassword(req.Password, user.PasswordHash) {
|
||||
_ = s.RecordLoginAttempt(ctx, req.Email, ipAddress, userAgent, "invalid_password", false)
|
||||
_ = s.incrementFailedAttempts(ctx, user.ID)
|
||||
return nil, fmt.Errorf("authentication failed")
|
||||
}
|
||||
|
||||
// Check user status
|
||||
if user.Status != domain.UserStatusActive {
|
||||
_ = s.RecordLoginAttempt(ctx, req.Email, ipAddress, userAgent, fmt.Sprintf("user_status_%s", user.Status), false)
|
||||
return nil, fmt.Errorf("account is not active")
|
||||
}
|
||||
|
||||
// Check email verification
|
||||
if !user.EmailVerified {
|
||||
_ = s.RecordLoginAttempt(ctx, req.Email, ipAddress, userAgent, "email_not_verified", false)
|
||||
return nil, fmt.Errorf("please verify your email address before logging in")
|
||||
}
|
||||
|
||||
// Handle two-factor authentication
|
||||
if user.TwoFactorEnabled {
|
||||
if req.TwoFactorCode == nil {
|
||||
// Generate temporary token for 2FA completion
|
||||
tempToken, err := s.generateTempTwoFactorToken(user, ipAddress, userAgent)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to generate temporary token: %w", err)
|
||||
}
|
||||
|
||||
return &domain.LoginResponse{
|
||||
RequiresTwoFactor: true,
|
||||
TwoFactorTempToken: &tempToken,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Validate 2FA code
|
||||
valid, err := s.validateTOTPCode(*req.TwoFactorCode, *user.TwoFactorSecret)
|
||||
if err != nil || !valid {
|
||||
_ = s.RecordLoginAttempt(ctx, req.Email, ipAddress, userAgent, "invalid_2fa_code", false)
|
||||
return nil, fmt.Errorf("invalid two-factor authentication code")
|
||||
}
|
||||
}
|
||||
|
||||
// Reset failed login attempts on successful login
|
||||
_ = s.resetFailedAttempts(ctx, user.ID)
|
||||
|
||||
// Create session
|
||||
session, err := s.createSession(ctx, user, ipAddress, userAgent, req.RememberMe)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create session: %w", err)
|
||||
}
|
||||
|
||||
// Generate JWT token
|
||||
token, expiresAt, err := s.generateJWTToken(user, session.ID.String(), false)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to generate token: %w", err)
|
||||
}
|
||||
|
||||
// Update last login
|
||||
_ = s.userRepo.UpdateLastLogin(ctx, user.ID)
|
||||
|
||||
// Record successful login
|
||||
_ = s.RecordLoginAttempt(ctx, req.Email, ipAddress, userAgent, "", true)
|
||||
|
||||
// Log successful login
|
||||
if s.auditRepo != nil {
|
||||
auditEvent := &interfaces.AuditEvent{
|
||||
ID: uuid.New(),
|
||||
Type: "auth.login",
|
||||
Severity: "info",
|
||||
Status: "success",
|
||||
Timestamp: time.Now().Format(time.RFC3339),
|
||||
ActorID: user.Email,
|
||||
ActorType: "user",
|
||||
ActorIP: ipAddress,
|
||||
UserAgent: userAgent,
|
||||
ResourceID: user.ID.String(),
|
||||
ResourceType: "user",
|
||||
Action: "login",
|
||||
Description: fmt.Sprintf("User %s logged in", user.Email),
|
||||
SessionID: session.ID.String(),
|
||||
Details: map[string]interface{}{
|
||||
"user_id": user.ID.String(),
|
||||
"session_id": session.ID.String(),
|
||||
"two_factor": user.TwoFactorEnabled,
|
||||
},
|
||||
}
|
||||
_ = s.auditRepo.LogEvent(ctx, auditEvent)
|
||||
}
|
||||
|
||||
return &domain.LoginResponse{
|
||||
User: user,
|
||||
Token: token,
|
||||
ExpiresAt: expiresAt,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Helper functions
|
||||
|
||||
func (s *authService) hashPassword(password string) (string, error) {
|
||||
bytes, err := bcrypt.GenerateFromPassword([]byte(password), s.bcryptCost)
|
||||
return string(bytes), err
|
||||
}
|
||||
|
||||
func (s *authService) verifyPassword(password, hash string) bool {
|
||||
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func (s *authService) generateSecureToken() (string, error) {
|
||||
bytes := make([]byte, 32)
|
||||
if _, err := rand.Read(bytes); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return base64.URLEncoding.EncodeToString(bytes), nil
|
||||
}
|
||||
|
||||
func (s *authService) generateJWTToken(user *domain.User, sessionID string, isTempToken bool) (string, time.Time, error) {
|
||||
expiresAt := time.Now().Add(s.tokenExpiry)
|
||||
if isTempToken {
|
||||
expiresAt = time.Now().Add(5 * time.Minute) // Temp tokens expire in 5 minutes
|
||||
}
|
||||
|
||||
claims := JWTClaims{
|
||||
UserID: user.ID.String(),
|
||||
Email: user.Email,
|
||||
Role: string(user.Role),
|
||||
SessionID: sessionID,
|
||||
TwoFactor: user.TwoFactorEnabled,
|
||||
TempToken: isTempToken,
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
Subject: user.ID.String(),
|
||||
Issuer: s.jwtIssuer,
|
||||
IssuedAt: jwt.NewNumericDate(time.Now()),
|
||||
ExpiresAt: jwt.NewNumericDate(expiresAt),
|
||||
},
|
||||
}
|
||||
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
tokenString, err := token.SignedString([]byte(s.jwtSecret))
|
||||
if err != nil {
|
||||
return "", time.Time{}, err
|
||||
}
|
||||
|
||||
return tokenString, expiresAt, nil
|
||||
}
|
||||
|
||||
func (s *authService) generateTempTwoFactorToken(user *domain.User, ipAddress, userAgent string) (string, error) {
|
||||
token, _, err := s.generateJWTToken(user, "", true)
|
||||
return token, err
|
||||
}
|
||||
|
||||
func (s *authService) createSession(ctx context.Context, user *domain.User, ipAddress, userAgent string, rememberMe bool) (*domain.UserSession, error) {
|
||||
expiresAt := time.Now().Add(s.tokenExpiry)
|
||||
if rememberMe {
|
||||
expiresAt = time.Now().Add(30 * 24 * time.Hour) // 30 days for remember me
|
||||
}
|
||||
|
||||
sessionToken, err := s.generateSecureToken()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
session := &domain.UserSession{
|
||||
ID: uuid.New(),
|
||||
UserID: user.ID,
|
||||
Token: sessionToken,
|
||||
IPAddress: ipAddress,
|
||||
UserAgent: userAgent,
|
||||
ExpiresAt: expiresAt,
|
||||
LastUsedAt: time.Now(),
|
||||
}
|
||||
|
||||
err = s.sessionRepo.Create(ctx, session)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return session, nil
|
||||
}
|
||||
|
||||
func (s *authService) incrementFailedAttempts(ctx context.Context, userID uuid.UUID) error {
|
||||
return s.userRepo.IncrementFailedAttempts(ctx, userID, s.lockoutDuration)
|
||||
}
|
||||
|
||||
func (s *authService) resetFailedAttempts(ctx context.Context, userID uuid.UUID) error {
|
||||
return s.userRepo.ResetFailedAttempts(ctx, userID)
|
||||
}
|
||||
|
||||
func (s *authService) validateTOTPCode(code, secret string) (bool, error) {
|
||||
return totp.Validate(code, secret, time.Now()), nil
|
||||
}
|
||||
|
||||
func (s *authService) generateBackupCode() (string, error) {
|
||||
// Generate 8-digit backup code
|
||||
bytes := make([]byte, 4)
|
||||
if _, err := rand.Read(bytes); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
code := ""
|
||||
for _, b := range bytes {
|
||||
code += fmt.Sprintf("%02d", int(b)%100)
|
||||
}
|
||||
|
||||
return code, nil
|
||||
}
|
||||
|
||||
// Utility function
|
||||
func timePtr(t time.Time) *time.Time {
|
||||
return &t
|
||||
}
|
||||
|
||||
// Placeholder implementations for other methods (to be completed)
|
||||
func (s *authService) Logout(ctx context.Context, token string) error {
|
||||
// TODO: Implement logout
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *authService) RefreshToken(ctx context.Context, token string) (*domain.LoginResponse, error) {
|
||||
// TODO: Implement refresh token
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (s *authService) ForgotPassword(ctx context.Context, req *domain.ForgotPasswordRequest, ipAddress, userAgent string) error {
|
||||
// TODO: Implement forgot password
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *authService) ResetPassword(ctx context.Context, req *domain.ResetPasswordRequest, ipAddress, userAgent string) error {
|
||||
// TODO: Implement reset password
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *authService) ChangePassword(ctx context.Context, userID uuid.UUID, req *domain.ChangePasswordRequest, actorID string) error {
|
||||
// TODO: Implement change password
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *authService) VerifyEmail(ctx context.Context, req *domain.VerifyEmailRequest, ipAddress, userAgent string) error {
|
||||
// TODO: Implement email verification
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *authService) ResendVerification(ctx context.Context, req *domain.ResendVerificationRequest, ipAddress, userAgent string) error {
|
||||
// TODO: Implement resend verification
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *authService) SetupTwoFactor(ctx context.Context, userID uuid.UUID) (*domain.SetupTwoFactorResponse, error) {
|
||||
user, err := s.userRepo.GetByID(ctx, userID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("user not found: %w", err)
|
||||
}
|
||||
|
||||
if user.TwoFactorEnabled {
|
||||
return nil, fmt.Errorf("two-factor authentication is already enabled")
|
||||
}
|
||||
|
||||
// Generate TOTP secret
|
||||
key, err := totp.Generate(totp.GenerateOpts{
|
||||
Issuer: s.jwtIssuer,
|
||||
AccountName: user.Email,
|
||||
SecretSize: 32,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to generate TOTP secret: %w", err)
|
||||
}
|
||||
|
||||
// Generate backup codes
|
||||
backupCodes := make([]string, 10)
|
||||
for i := 0; i < 10; i++ {
|
||||
code, err := s.generateBackupCode()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to generate backup code: %w", err)
|
||||
}
|
||||
backupCodes[i] = code
|
||||
}
|
||||
|
||||
// Create QR code URL
|
||||
qrCodeURL := key.URL()
|
||||
|
||||
return &domain.SetupTwoFactorResponse{
|
||||
Secret: key.Secret(),
|
||||
QRCodeURL: qrCodeURL,
|
||||
BackupCodes: backupCodes,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *authService) EnableTwoFactor(ctx context.Context, userID uuid.UUID, req *domain.EnableTwoFactorRequest, actorID string) error {
|
||||
user, err := s.userRepo.GetByID(ctx, userID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("user not found: %w", err)
|
||||
}
|
||||
|
||||
if user.TwoFactorEnabled {
|
||||
return fmt.Errorf("two-factor authentication is already enabled")
|
||||
}
|
||||
|
||||
if user.TwoFactorSecret == nil {
|
||||
return fmt.Errorf("two-factor authentication setup required first")
|
||||
}
|
||||
|
||||
// Validate the provided code
|
||||
valid, err := s.validateTOTPCode(req.Code, *user.TwoFactorSecret)
|
||||
if err != nil || !valid {
|
||||
return fmt.Errorf("invalid verification code")
|
||||
}
|
||||
|
||||
// Generate backup codes
|
||||
backupCodes := make([]string, 10)
|
||||
for i := 0; i < 10; i++ {
|
||||
code, err := s.generateBackupCode()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to generate backup code: %w", err)
|
||||
}
|
||||
backupCodes[i] = code
|
||||
}
|
||||
|
||||
// Enable 2FA in database
|
||||
err = s.userRepo.UpdateTwoFactorSettings(ctx, userID, true, user.TwoFactorSecret, backupCodes)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to enable two-factor authentication: %w", err)
|
||||
}
|
||||
|
||||
// Log audit event
|
||||
if s.auditRepo != nil {
|
||||
auditEvent := &interfaces.AuditEvent{
|
||||
ID: uuid.New(),
|
||||
Type: "auth.2fa_enabled",
|
||||
Severity: "info",
|
||||
Status: "success",
|
||||
Timestamp: time.Now().Format(time.RFC3339),
|
||||
ActorID: actorID,
|
||||
ActorType: "user",
|
||||
ResourceID: userID.String(),
|
||||
ResourceType: "user",
|
||||
Action: "enable_2fa",
|
||||
Description: fmt.Sprintf("Two-factor authentication enabled for user %s", user.Email),
|
||||
Details: map[string]interface{}{
|
||||
"user_id": userID.String(),
|
||||
"email": user.Email,
|
||||
},
|
||||
}
|
||||
_ = s.auditRepo.LogEvent(ctx, auditEvent)
|
||||
}
|
||||
|
||||
s.logger.Info("Two-factor authentication enabled",
|
||||
zap.String("user_id", userID.String()),
|
||||
zap.String("email", user.Email),
|
||||
zap.String("actor", actorID))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *authService) DisableTwoFactor(ctx context.Context, userID uuid.UUID, req *domain.DisableTwoFactorRequest, actorID string) error {
|
||||
// TODO: Implement disable 2FA
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *authService) ValidateTwoFactor(ctx context.Context, req *domain.ValidateTwoFactorRequest, ipAddress, userAgent string) (*domain.LoginResponse, error) {
|
||||
// TODO: Implement validate 2FA
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (s *authService) RegenerateTwoFactorBackupCodes(ctx context.Context, userID uuid.UUID, actorID string) ([]string, error) {
|
||||
// TODO: Implement regenerate backup codes
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (s *authService) GetUserSessions(ctx context.Context, userID uuid.UUID) (*domain.ListSessionsResponse, error) {
|
||||
// TODO: Implement get user sessions
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (s *authService) RevokeSession(ctx context.Context, userID uuid.UUID, sessionID uuid.UUID, actorID string) error {
|
||||
// TODO: Implement revoke session
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *authService) RevokeAllSessions(ctx context.Context, userID uuid.UUID, actorID string) error {
|
||||
// TODO: Implement revoke all sessions
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *authService) ValidateToken(ctx context.Context, tokenString string) (*domain.AuthContext, error) {
|
||||
// TODO: Implement token validation
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (s *authService) IsAccountLocked(ctx context.Context, email string) (bool, *time.Time, error) {
|
||||
user, err := s.userRepo.GetByEmail(ctx, email)
|
||||
if err != nil {
|
||||
return false, nil, nil // User doesn't exist, not locked
|
||||
}
|
||||
|
||||
// Check if account is currently locked
|
||||
if user.LockedUntil != nil && time.Now().Before(*user.LockedUntil) {
|
||||
return true, user.LockedUntil, nil
|
||||
}
|
||||
|
||||
// Clear expired lock
|
||||
if user.LockedUntil != nil && time.Now().After(*user.LockedUntil) {
|
||||
_ = s.userRepo.ResetFailedAttempts(ctx, user.ID)
|
||||
}
|
||||
|
||||
return false, nil, nil
|
||||
}
|
||||
|
||||
func (s *authService) RecordLoginAttempt(ctx context.Context, email, ipAddress, userAgent, failureReason string, success bool) error {
|
||||
attempt := &domain.LoginAttempt{
|
||||
ID: uuid.New(),
|
||||
Email: email,
|
||||
IPAddress: ipAddress,
|
||||
UserAgent: &userAgent,
|
||||
Success: success,
|
||||
FailureReason: &failureReason,
|
||||
AttemptedAt: time.Now(),
|
||||
}
|
||||
|
||||
if failureReason == "" {
|
||||
attempt.FailureReason = nil
|
||||
}
|
||||
|
||||
// In a full implementation, you would use a LoginAttemptRepository
|
||||
// For now, just log the audit event
|
||||
if s.auditRepo != nil {
|
||||
auditEvent := &interfaces.AuditEvent{
|
||||
ID: uuid.New(),
|
||||
Type: "auth.login_attempt",
|
||||
Severity: "info",
|
||||
Status: "success",
|
||||
Timestamp: time.Now().Format(time.RFC3339),
|
||||
ActorID: email,
|
||||
ActorType: "user",
|
||||
ActorIP: ipAddress,
|
||||
UserAgent: userAgent,
|
||||
Action: "login_attempt",
|
||||
Description: fmt.Sprintf("Login attempt for %s: success=%v", email, success),
|
||||
Details: map[string]interface{}{
|
||||
"email": email,
|
||||
"success": success,
|
||||
"failure_reason": failureReason,
|
||||
},
|
||||
}
|
||||
_ = s.auditRepo.LogEvent(ctx, auditEvent)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user