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\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();
|
||||||
|
|
||||||
|
|
|
@ -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)) {
|
||||||
|
|
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->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');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue