Compare commits
3 Commits
f7e4aeb898
...
f549940249
Author | SHA1 | Date |
---|---|---|
|
f549940249 | |
|
fee0616ca4 | |
|
626fc4ba2b |
|
@ -0,0 +1,91 @@
|
|||
<?php
|
||||
|
||||
class RateLimiter {
|
||||
private $db;
|
||||
private $maxAttempts = 5; // Maximum login attempts
|
||||
private $decayMinutes = 15; // Time window in minutes
|
||||
private $tableName = 'login_attempts';
|
||||
|
||||
public function __construct($database) {
|
||||
$this->db = $database->getConnection();
|
||||
$this->createTableIfNotExists();
|
||||
}
|
||||
|
||||
private function createTableIfNotExists() {
|
||||
$sql = "CREATE TABLE IF NOT EXISTS {$this->tableName} (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
ip_address VARCHAR(45) NOT NULL,
|
||||
username VARCHAR(255) NOT NULL,
|
||||
attempted_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
INDEX idx_ip_username (ip_address, username)
|
||||
)";
|
||||
|
||||
$this->db->exec($sql);
|
||||
}
|
||||
|
||||
public function attempt($username, $ipAddress) {
|
||||
// Clean old attempts
|
||||
$this->clearOldAttempts();
|
||||
|
||||
// Record this attempt
|
||||
$sql = "INSERT INTO {$this->tableName} (ip_address, username) VALUES (:ip, :username)";
|
||||
$stmt = $this->db->prepare($sql);
|
||||
$stmt->execute([
|
||||
':ip' => $ipAddress,
|
||||
':username' => $username
|
||||
]);
|
||||
|
||||
// Check if too many attempts
|
||||
return !$this->tooManyAttempts($username, $ipAddress);
|
||||
}
|
||||
|
||||
public function tooManyAttempts($username, $ipAddress) {
|
||||
$sql = "SELECT COUNT(*) as attempts
|
||||
FROM {$this->tableName}
|
||||
WHERE ip_address = :ip
|
||||
AND username = :username
|
||||
AND attempted_at > datetime('now', '-' || :minutes || ' minutes')";
|
||||
|
||||
$stmt = $this->db->prepare($sql);
|
||||
$stmt->execute([
|
||||
':ip' => $ipAddress,
|
||||
':username' => $username,
|
||||
':minutes' => $this->decayMinutes
|
||||
]);
|
||||
|
||||
$result = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
return $result['attempts'] >= $this->maxAttempts;
|
||||
}
|
||||
|
||||
public function clearOldAttempts() {
|
||||
$sql = "DELETE FROM {$this->tableName}
|
||||
WHERE attempted_at < datetime('now', '-' || :minutes || ' minutes')";
|
||||
|
||||
$stmt = $this->db->prepare($sql);
|
||||
$stmt->execute([
|
||||
':minutes' => $this->decayMinutes
|
||||
]);
|
||||
}
|
||||
|
||||
public function getRemainingAttempts($username, $ipAddress) {
|
||||
$sql = "SELECT COUNT(*) as attempts
|
||||
FROM {$this->tableName}
|
||||
WHERE ip_address = :ip
|
||||
AND username = :username
|
||||
AND attempted_at > datetime('now', '-' || :minutes || ' minutes')";
|
||||
|
||||
$stmt = $this->db->prepare($sql);
|
||||
$stmt->execute([
|
||||
':ip' => $ipAddress,
|
||||
':username' => $username,
|
||||
':minutes' => $this->decayMinutes
|
||||
]);
|
||||
|
||||
$result = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
return max(0, $this->maxAttempts - $result['attempts']);
|
||||
}
|
||||
|
||||
public function getDecayMinutes() {
|
||||
return $this->decayMinutes;
|
||||
}
|
||||
}
|
|
@ -10,6 +10,7 @@ class User {
|
|||
* @var PDO|null $db The database connection instance.
|
||||
*/
|
||||
private $db;
|
||||
private $ratelimiter;
|
||||
|
||||
/**
|
||||
* User constructor.
|
||||
|
@ -19,6 +20,7 @@ class User {
|
|||
*/
|
||||
public function __construct($database) {
|
||||
$this->db = $database->getConnection();
|
||||
$this->ratelimiter = new RateLimiter($database);
|
||||
}
|
||||
|
||||
|
||||
|
@ -88,17 +90,33 @@ class User {
|
|||
* @return bool True if login is successful, false otherwise.
|
||||
*/
|
||||
public function login($username, $password) {
|
||||
// get client IP address
|
||||
$ipAddress = $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';
|
||||
|
||||
// check rate limiting
|
||||
if (!$this->rateLimiter->attempt($username, $ipAddress)) {
|
||||
$remainingTime = $this->rateLimiter->getDecayMinutes();
|
||||
throw new Exception("Too many login attempts. Please try again in {$remainingTime} minutes.");
|
||||
}
|
||||
|
||||
$query = $this->db->prepare("SELECT * FROM users WHERE username = :username");
|
||||
$query->bindParam(':username', $username);
|
||||
$query->execute();
|
||||
|
||||
$user = $query->fetch(PDO::FETCH_ASSOC);
|
||||
if ( $user && password_verify($password, $user['password'])) {
|
||||
if ($user && password_verify($password, $user['password'])) {
|
||||
$_SESSION['user_id'] = $user['id'];
|
||||
$_SESSION['username'] = $user['username'];
|
||||
return true;
|
||||
}
|
||||
|
||||
// Login failed, return remaining attempts info
|
||||
$remainingAttempts = $this->rateLimiter->getRemainingAttempts($username, $ipAddress);
|
||||
if ($remainingAttempts > 0) {
|
||||
throw new Exception("Invalid credentials. {$remainingAttempts} attempts remaining.");
|
||||
} else {
|
||||
return false;
|
||||
$remainingTime = $this->rateLimiter->getDecayMinutes();
|
||||
throw new Exception("Too many login attempts. Please try again in {$remainingTime} minutes.");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -22,6 +22,7 @@ try {
|
|||
$dbWeb = connectDB($config);
|
||||
|
||||
if ( $_SERVER['REQUEST_METHOD'] == 'POST' ) {
|
||||
try {
|
||||
$username = $_POST['username'];
|
||||
$password = $_POST['password'];
|
||||
|
||||
|
@ -66,6 +67,9 @@ try {
|
|||
header('Location: ' . htmlspecialchars($app_root));
|
||||
exit();
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
$error = $e->getMessage();
|
||||
}
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
$error = getError('There was an unexpected error. Please try again.', $e->getMessage());
|
||||
|
|
Loading…
Reference in New Issue