true exige HTTPS. Si vous testez en HTTP pur // sur une VM sans certificat, passez temporairement cette valeur a // false, sinon le cookie de session ne sera jamais transmis et la // connexion semblera "ne pas fonctionner" (boucle de redirection vers // login.php). Remettez-le a true avant la mise en production reelle. session_set_cookie_params([ 'lifetime' => 0, 'path' => '/admin', 'secure' => true, 'httponly' => true, 'samesite' => 'Strict', ]); session_start(); } require_once __DIR__ . '/../lib/csrf.php'; function require_admin_auth(): void { if (empty($_SESSION['admin_user_id'])) { header('Location: /admin/login.php'); exit; } if (!empty($_SESSION['last_activity']) && (time() - $_SESSION['last_activity']) > SESSION_IDLE_TIMEOUT) { session_unset(); session_destroy(); header('Location: /admin/login.php?timeout=1'); exit; } $_SESSION['last_activity'] = time(); } function current_admin_username(): string { return $_SESSION['admin_username'] ?? 'unknown'; } /** * IP du visiteur, lue exclusivement depuis REMOTE_ADDR. Ne fait * volontairement pas confiance a X-Forwarded-For : sur cette * installation, Apache recoit les connexions directement (ou via * Tailscale), sans reverse proxy declare qui nettoierait cet en-tete. * Un client pourrait sinon forger cet en-tete pour usurper une IP et * contourner le rate limiting. */ function client_ip_address(): string { return $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0'; } function normalize_login_identifier(string $identifier): string { return mb_strtolower(trim($identifier)); } /** * Verifie si l'identifiant+IP OU l'IP seule ont depasse leur seuil * respectif d'echecs sur la fenetre glissante. Stocke en base, donc * non contournable en changeant de session/cookie cote client. */ function is_login_locked_out(PDO $db, string $identifier): bool { $normalized = normalize_login_identifier($identifier); $ip = client_ip_address(); $windowStart = date('Y-m-d H:i:s', time() - LOGIN_ATTEMPTS_WINDOW_SECONDS); // Seuil cible : ce compte precis, depuis cette IP precise. $accountStmt = $db->prepare( 'SELECT COUNT(*), MAX(attempted_at) FROM login_attempts WHERE username_normalized = ? AND ip_address = ? AND success = 0 AND attempted_at >= ?' ); $accountStmt->execute([$normalized, $ip, $windowStart]); [$accountFailures, $accountLastAttempt] = $accountStmt->fetch(PDO::FETCH_NUM); if ((int)$accountFailures >= MAX_LOGIN_ATTEMPTS_PER_ACCOUNT && $accountLastAttempt !== null && (time() - strtotime($accountLastAttempt)) < LOCKOUT_SECONDS_PER_ACCOUNT ) { return true; } // Seuil large : toute cette IP, tous identifiants confondus. $ipStmt = $db->prepare( 'SELECT COUNT(*), MAX(attempted_at) FROM login_attempts WHERE ip_address = ? AND success = 0 AND attempted_at >= ?' ); $ipStmt->execute([$ip, $windowStart]); [$ipFailures, $ipLastAttempt] = $ipStmt->fetch(PDO::FETCH_NUM); if ((int)$ipFailures >= MAX_LOGIN_ATTEMPTS_PER_IP && $ipLastAttempt !== null && (time() - strtotime($ipLastAttempt)) < LOCKOUT_SECONDS_PER_IP ) { return true; } return false; } function record_login_attempt(PDO $db, string $identifier, bool $success, bool $turnstileVerified = false): void { $stmt = $db->prepare( 'INSERT INTO login_attempts (username_normalized, ip_address, success, turnstile_verified) VALUES (?, ?, ?, ?)' ); $stmt->execute([ normalize_login_identifier($identifier), client_ip_address(), $success ? 1 : 0, $turnstileVerified ? 1 : 0, ]); } function attempt_admin_login(PDO $db, string $username, string $password): bool { $stmt = $db->prepare('SELECT id, username, password_hash FROM admin_users WHERE username = ?'); $stmt->execute([$username]); $user = $stmt->fetch(PDO::FETCH_ASSOC); if ($user && password_verify($password, $user['password_hash'])) { $_SESSION['admin_user_id'] = $user['id']; $_SESSION['admin_username'] = $user['username']; $_SESSION['last_activity'] = time(); session_regenerate_id(true); return true; } return false; }