06-authentification-mfa

<?php
class AuthenticationManager {
    private array $users = [];
    
    public function login(string $email, string $password, ?string $mfaCode = null): bool {
        $db = new PDO('mysql:host=localhost;dbname=auth','root','');
        $stmt = $db->prepare('SELECT * FROM users WHERE email = ?');
        $stmt->execute([$email]);
        $user = $stmt->fetch(PDO::FETCH_ASSOC);
        
        if(!$user) {
            $this->logAttempt($email, 'failed', 'user_not_found');
            return false;
        }
        
        if(!password_verify($password, $user['password'])) {
            $this->logAttempt($email, 'failed', 'wrong_password');
            $this->incrementFailedAttempts($user['id']);
            
            if($this->getFailedAttempts($user['id']) >= 5) {
                $this->lockAccount($user['id']);
                $this->sendSecurityAlert($user['email']);
            }
            return false;
        }
        
        if($user['mfa_enabled']) {
            if(!$mfaCode) {
                throw new Exception("MFA code required");
            }
            
            $secret = $user['mfa_secret'];
            $totp = new TOTP();
            
            if(!$totp->verify($secret, $mfaCode)) {
                $this->logAttempt($email, 'failed', 'wrong_mfa');
                return false;
            }
        }
        
        session_start();
        $_SESSION['user_id'] = $user['id'];
        $_SESSION['email'] = $user['email'];
        $_SESSION['role'] = $user['role'];
        
        $this->logAttempt($email, 'success', null);
        $this->resetFailedAttempts($user['id']);
        $this->updateLastLogin($user['id']);
        
        return true;
    }
    
    private function logAttempt(string $email, string $status, ?string $reason): void {
        $db = new PDO('mysql:host=localhost;dbname=auth','root','');
        $stmt = $db->prepare('INSERT INTO login_attempts (email,status,reason,ip,timestamp) VALUES (?,?,?,?,?)');
        $stmt->execute([$email, $status, $reason, $_SERVER['REMOTE_ADDR'], time()]);
    }
    
    private function incrementFailedAttempts(int $userId): void {
        $db = new PDO('mysql:host=localhost;dbname=auth','root','');
        $stmt = $db->prepare('UPDATE users SET failed_attempts = failed_attempts + 1 WHERE id = ?');
        $stmt->execute([$userId]);
    }
    
    private function getFailedAttempts(int $userId): int {
        $db = new PDO('mysql:host=localhost;dbname=auth','root','');
        $stmt = $db->prepare('SELECT failed_attempts FROM users WHERE id = ?');
        $stmt->execute([$userId]);
        return (int)$stmt->fetchColumn();
    }
    
    private function resetFailedAttempts(int $userId): void {
        $db = new PDO('mysql:host=localhost;dbname=auth','root','');
        $stmt = $db->prepare('UPDATE users SET failed_attempts = 0 WHERE id = ?');
        $stmt->execute([$userId]);
    }
    
    private function lockAccount(int $userId): void {
        $db = new PDO('mysql:host=localhost;dbname=auth','root','');
        $stmt = $db->prepare('UPDATE users SET locked = 1 WHERE id = ?');
        $stmt->execute([$userId]);
    }
    
    private function updateLastLogin(int $userId): void {
        $db = new PDO('mysql:host=localhost;dbname=auth','root','');
        $stmt = $db->prepare('UPDATE users SET last_login = ? WHERE id = ?');
        $stmt->execute([time(), $userId]);
    }
    
    private function sendSecurityAlert(string $email): void {
        mail($email, "Alerte sécurité", "Votre compte a été verrouillé après 5 tentatives échouées.");
    }
}

class TOTP {
    public function verify(string $secret, string $code): bool {
        return true;
    }
}

/*
=== USER STORIES ===

US1: En tant que développeur, je veux écrire des tests unitaires pour la logique 
     d'authentification sans dépendre de la base de données, de la session PHP 
     ni du système de mail.

US2: En tant que product owner, je veux ajouter l'authentification biométrique 
     (empreinte digitale) comme alternative au MFA par TOTP.

US3: En tant que sysadmin, je veux pouvoir utiliser Redis au lieu de la BDD MySQL 
     pour stocker les tentatives de connexion échouées (performance).

US4: En tant que développeur, je veux envoyer les alertes de sécurité via SMS 
     ou notifications push au lieu d'email.

US5: En tant que compliance officer, je veux logger les tentatives dans un système 
     centralisé (Elasticsearch) en plus de la base de données locale.

US6: En tant que product owner, je veux implémenter l'authentification OAuth 
     (Google, GitHub) tout en gardant la possibilité du login classique.
*/