Add login rate limiting.
This commit is contained in:
parent
d058036da1
commit
877a7b1d47
5 changed files with 145 additions and 3 deletions
48
database/2018_03_22_040816_create_login_attempts_table.php
Normal file
48
database/2018_03_22_040816_create_login_attempts_table.php
Normal file
|
@ -0,0 +1,48 @@
|
|||
<?php
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Misuzu\Database;
|
||||
|
||||
// phpcs:disable
|
||||
class CreateLoginAttemptsTable extends Migration
|
||||
{
|
||||
/**
|
||||
* @SuppressWarnings(PHPMD)
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
$schema = Database::connection()->getSchemaBuilder();
|
||||
$schema->create('login_attempts', function (Blueprint $table) {
|
||||
$table->increments('attempt_id');
|
||||
|
||||
$table->boolean('was_successful');
|
||||
|
||||
$table->binary('attempt_ip');
|
||||
|
||||
$table->char('attempt_country', 2)
|
||||
->default('XX');
|
||||
|
||||
$table->integer('user_id')
|
||||
->nullable()
|
||||
->default(null)
|
||||
->unsigned();
|
||||
|
||||
$table->timestamps();
|
||||
|
||||
$table->foreign('user_id')
|
||||
->references('user_id')
|
||||
->on('users')
|
||||
->onUpdate('cascade')
|
||||
->onDelete('set null');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @SuppressWarnings(PHPMD)
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
$schema = Database::connection()->getSchemaBuilder();
|
||||
$schema->drop('login_attempts');
|
||||
}
|
||||
}
|
|
@ -6,6 +6,7 @@ use Misuzu\Database;
|
|||
use Misuzu\Net\IPAddress;
|
||||
use Misuzu\Users\User;
|
||||
use Misuzu\Users\Session;
|
||||
use Misuzu\Users\LoginAttempt;
|
||||
|
||||
require_once __DIR__ . '/../misuzu.php';
|
||||
|
||||
|
@ -61,27 +62,43 @@ switch ($mode) {
|
|||
$auth_login_error = '';
|
||||
|
||||
while ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$ipAddress = IPAddress::remote();
|
||||
|
||||
if (!isset($_POST['username'], $_POST['password'])) {
|
||||
$auth_login_error = "You didn't fill all the forms!";
|
||||
break;
|
||||
}
|
||||
|
||||
$loginAttempts = LoginAttempt::fromIpAddress(IPAddress::remote())
|
||||
->where('was_successful', false)
|
||||
->where('created_at', '>', Carbon::now()->subHour()->toDateTimeString())
|
||||
->get();
|
||||
|
||||
if ($loginAttempts->count() >= 5) {
|
||||
$auth_login_error = 'Too many failed login attempts, try again later.';
|
||||
break;
|
||||
}
|
||||
|
||||
$username = $_POST['username'] ?? '';
|
||||
$password = $_POST['password'] ?? '';
|
||||
|
||||
try {
|
||||
$user = User::where('username', $username)->orWhere('email', $username)->firstOrFail();
|
||||
} catch (ModelNotFoundException $e) {
|
||||
LoginAttempt::recordFail($ipAddress);
|
||||
$auth_login_error = 'Invalid username or password!';
|
||||
break;
|
||||
}
|
||||
|
||||
if (!$user->validatePassword($password)) {
|
||||
LoginAttempt::recordFail($ipAddress, $user);
|
||||
$auth_login_error = 'Invalid username or password!';
|
||||
break;
|
||||
}
|
||||
|
||||
$session = Session::createSession($user, 'Misuzu T2');
|
||||
LoginAttempt::recordSuccess($ipAddress, $user);
|
||||
|
||||
$session = Session::createSession($user, 'Misuzu T2', null, $ipAddress);
|
||||
$app->setSession($session);
|
||||
set_cookie_m('uid', $session->user_id, 604800);
|
||||
set_cookie_m('sid', $session->session_key, 604800);
|
||||
|
@ -89,7 +106,7 @@ switch ($mode) {
|
|||
// Temporary key generation for chat login.
|
||||
// Should eventually be replaced with a callback login system.
|
||||
// Also uses different cookies since $httponly is required to be false for these.
|
||||
$user->last_ip = IPAddress::remote();
|
||||
$user->last_ip = $ipAddress;
|
||||
$user->user_chat_key = bin2hex(random_bytes(16));
|
||||
$user->save();
|
||||
|
||||
|
|
|
@ -39,6 +39,11 @@ final class IPAddress
|
|||
return inet_ntop($this->ipRaw);
|
||||
}
|
||||
|
||||
public function getCountryCode(): string
|
||||
{
|
||||
return get_country_code($this->getString());
|
||||
}
|
||||
|
||||
public function __construct(int $version, string $rawIp)
|
||||
{
|
||||
if (!array_key_exists($version, self::BYTE_COUNT)) {
|
||||
|
|
67
src/Users/LoginAttempt.php
Normal file
67
src/Users/LoginAttempt.php
Normal file
|
@ -0,0 +1,67 @@
|
|||
<?php
|
||||
namespace Misuzu\Users;
|
||||
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Misuzu\Model;
|
||||
use Misuzu\Net\IPAddress;
|
||||
|
||||
class LoginAttempt extends Model
|
||||
{
|
||||
protected $primaryKey = 'attempt_id';
|
||||
|
||||
public static function recordSuccess(IPAddress $ipAddress, User $user): LoginAttempt
|
||||
{
|
||||
return static::recordAttempt(true, $ipAddress, $user);
|
||||
}
|
||||
|
||||
public static function recordFail(IPAddress $ipAddress, ?User $user = null): LoginAttempt
|
||||
{
|
||||
return static::recordAttempt(false, $ipAddress, $user);
|
||||
}
|
||||
|
||||
public static function recordAttempt(bool $success, IPAddress $ipAddress, ?User $user = null): LoginAttempt
|
||||
{
|
||||
$attempt = new static;
|
||||
$attempt->was_successful = $success;
|
||||
$attempt->attempt_ip = $ipAddress;
|
||||
|
||||
if ($user !== null) {
|
||||
$attempt->user_id = $user;
|
||||
}
|
||||
|
||||
$attempt->save();
|
||||
|
||||
return $attempt;
|
||||
}
|
||||
|
||||
public static function fromIpAddress(IPAddress $ipAddress): Builder
|
||||
{
|
||||
return static::where('attempt_ip', $ipAddress->getRaw());
|
||||
}
|
||||
|
||||
public function setAttemptIpAttribute(IPAddress $ipAddress): void
|
||||
{
|
||||
$this->attributes['attempt_ip'] = $ipAddress->getRaw();
|
||||
$this->attributes['attempt_country'] = $ipAddress->getCountryCode();
|
||||
}
|
||||
|
||||
public function getAttemptIpAttribute(string $ipAddress): IPAddress
|
||||
{
|
||||
return IPAddress::fromRaw($ipAddress);
|
||||
}
|
||||
|
||||
public function setUserIdAttribute(User $user): void
|
||||
{
|
||||
$this->attributes['user_id'] = $user->user_id;
|
||||
}
|
||||
|
||||
public function getUserIdAttribute(int $userId): User
|
||||
{
|
||||
return User::findOrFail($userId);
|
||||
}
|
||||
|
||||
public function user()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'user_id');
|
||||
}
|
||||
}
|
|
@ -34,7 +34,7 @@ class User extends Model
|
|||
$user->email = $email;
|
||||
$user->register_ip = $ipAddress;
|
||||
$user->last_ip = $ipAddress;
|
||||
$user->user_country = get_country_code($ipAddress->getString());
|
||||
$user->user_country = $ipAddress->getCountryCode();
|
||||
$user->save();
|
||||
|
||||
return $user;
|
||||
|
@ -173,4 +173,9 @@ class User extends Model
|
|||
{
|
||||
return $this->hasMany(UserRole::class, 'user_id');
|
||||
}
|
||||
|
||||
public function loginAttempts()
|
||||
{
|
||||
return $this->hasMany(LoginAttempt::class, 'user_id');
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue