654 lines
20 KiB
Go
654 lines
20 KiB
Go
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
|
|
} |