Add login rate limiting.

This commit is contained in:
flash 2018-03-22 04:45:59 +01:00
parent d058036da1
commit 877a7b1d47
5 changed files with 145 additions and 3 deletions

View 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');
}
}

View file

@ -6,6 +6,7 @@ use Misuzu\Database;
use Misuzu\Net\IPAddress; use Misuzu\Net\IPAddress;
use Misuzu\Users\User; use Misuzu\Users\User;
use Misuzu\Users\Session; use Misuzu\Users\Session;
use Misuzu\Users\LoginAttempt;
require_once __DIR__ . '/../misuzu.php'; require_once __DIR__ . '/../misuzu.php';
@ -61,27 +62,43 @@ switch ($mode) {
$auth_login_error = ''; $auth_login_error = '';
while ($_SERVER['REQUEST_METHOD'] === 'POST') { while ($_SERVER['REQUEST_METHOD'] === 'POST') {
$ipAddress = IPAddress::remote();
if (!isset($_POST['username'], $_POST['password'])) { if (!isset($_POST['username'], $_POST['password'])) {
$auth_login_error = "You didn't fill all the forms!"; $auth_login_error = "You didn't fill all the forms!";
break; 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'] ?? ''; $username = $_POST['username'] ?? '';
$password = $_POST['password'] ?? ''; $password = $_POST['password'] ?? '';
try { try {
$user = User::where('username', $username)->orWhere('email', $username)->firstOrFail(); $user = User::where('username', $username)->orWhere('email', $username)->firstOrFail();
} catch (ModelNotFoundException $e) { } catch (ModelNotFoundException $e) {
LoginAttempt::recordFail($ipAddress);
$auth_login_error = 'Invalid username or password!'; $auth_login_error = 'Invalid username or password!';
break; break;
} }
if (!$user->validatePassword($password)) { if (!$user->validatePassword($password)) {
LoginAttempt::recordFail($ipAddress, $user);
$auth_login_error = 'Invalid username or password!'; $auth_login_error = 'Invalid username or password!';
break; break;
} }
$session = Session::createSession($user, 'Misuzu T2'); LoginAttempt::recordSuccess($ipAddress, $user);
$session = Session::createSession($user, 'Misuzu T2', null, $ipAddress);
$app->setSession($session); $app->setSession($session);
set_cookie_m('uid', $session->user_id, 604800); set_cookie_m('uid', $session->user_id, 604800);
set_cookie_m('sid', $session->session_key, 604800); set_cookie_m('sid', $session->session_key, 604800);
@ -89,7 +106,7 @@ switch ($mode) {
// Temporary key generation for chat login. // Temporary key generation for chat login.
// Should eventually be replaced with a callback login system. // Should eventually be replaced with a callback login system.
// Also uses different cookies since $httponly is required to be false for these. // 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->user_chat_key = bin2hex(random_bytes(16));
$user->save(); $user->save();

View file

@ -39,6 +39,11 @@ final class IPAddress
return inet_ntop($this->ipRaw); return inet_ntop($this->ipRaw);
} }
public function getCountryCode(): string
{
return get_country_code($this->getString());
}
public function __construct(int $version, string $rawIp) public function __construct(int $version, string $rawIp)
{ {
if (!array_key_exists($version, self::BYTE_COUNT)) { if (!array_key_exists($version, self::BYTE_COUNT)) {

View 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');
}
}

View file

@ -34,7 +34,7 @@ class User extends Model
$user->email = $email; $user->email = $email;
$user->register_ip = $ipAddress; $user->register_ip = $ipAddress;
$user->last_ip = $ipAddress; $user->last_ip = $ipAddress;
$user->user_country = get_country_code($ipAddress->getString()); $user->user_country = $ipAddress->getCountryCode();
$user->save(); $user->save();
return $user; return $user;
@ -173,4 +173,9 @@ class User extends Model
{ {
return $this->hasMany(UserRole::class, 'user_id'); return $this->hasMany(UserRole::class, 'user_id');
} }
public function loginAttempts()
{
return $this->hasMany(LoginAttempt::class, 'user_id');
}
} }