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.
*/