feat(auth): lock admin login per account as well as per IP
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
7f41c27958
commit
fc765ad007
@@ -29,9 +29,10 @@ class LoginThrottleTest extends TestCase
|
||||
(new TemplateSeeder())->seedScenarioIfMissing('admin_login_alert');
|
||||
}
|
||||
|
||||
private function login(string $username, string $password)
|
||||
private function login(string $username, string $password, string $ip = '127.0.0.1')
|
||||
{
|
||||
return $this->postJson('/api/v1/administrator/login', ['username' => $username, 'password' => $password]);
|
||||
return $this->withServerVariables(['REMOTE_ADDR' => $ip])
|
||||
->postJson('/api/v1/administrator/login', ['username' => $username, 'password' => $password]);
|
||||
}
|
||||
|
||||
private function staff(): User
|
||||
@@ -49,7 +50,7 @@ class LoginThrottleTest extends TestCase
|
||||
|
||||
$this->login('staff', 'wrong')->assertStatus(429);
|
||||
$this->login('staff', 'correct-password-123')->assertStatus(429);
|
||||
$this->login('someone-else', 'whatever')->assertStatus(429);
|
||||
$this->login('someone-else', 'whatever')->assertStatus(429)->assertJsonPath('message', fn ($m) => str_starts_with($m, 'IP của bạn'));
|
||||
|
||||
$this->travel(21)->minutes();
|
||||
$this->login('staff', 'correct-password-123')->assertOk();
|
||||
@@ -67,7 +68,22 @@ class LoginThrottleTest extends TestCase
|
||||
}
|
||||
|
||||
$this->login('victim', 'guess-final')->assertStatus(429);
|
||||
$this->assertTrue(AdminLoginBlock::sole()->isBlocked());
|
||||
$this->assertTrue(AdminLoginBlock::where('kind', 'ip')->sole()->isBlocked());
|
||||
}
|
||||
|
||||
public function test_account_is_locked_after_failures_from_different_ips(): void
|
||||
{
|
||||
$this->staff();
|
||||
|
||||
foreach (['10.0.0.1', '10.0.0.2', '10.0.0.3', '10.0.0.4'] as $ip) {
|
||||
$this->login('staff', 'wrong', $ip)->assertUnauthorized();
|
||||
}
|
||||
$this->login('STAFF', 'wrong', '10.0.0.5')->assertStatus(429);
|
||||
|
||||
$this->login('staff', 'correct-password-123', '10.0.0.9')
|
||||
->assertStatus(429)
|
||||
->assertJsonPath('message', fn ($m) => str_starts_with($m, 'Tài khoản này'));
|
||||
$this->login('another', 'whatever', '10.0.0.9')->assertUnauthorized();
|
||||
}
|
||||
|
||||
public function test_successful_login_alerts_configured_recipients(): void
|
||||
@@ -102,15 +118,19 @@ class LoginThrottleTest extends TestCase
|
||||
$admin = User::factory()->create();
|
||||
$admin->assignRole('Full Administrator');
|
||||
|
||||
$id = $this->actingAs($admin, 'admin')->getJson('/api/v1/administrator/login-blocks')
|
||||
$items = collect($this->actingAs($admin, 'admin')->getJson('/api/v1/administrator/login-blocks')
|
||||
->assertOk()
|
||||
->assertJsonPath('data.items.0.ip', '127.0.0.1')
|
||||
->assertJsonPath('data.items.0.last_username', 'staff')
|
||||
->json('data.items.0.id');
|
||||
->assertJsonCount(2, 'data.items')
|
||||
->json('data.items'))->keyBy('kind');
|
||||
$this->assertSame('127.0.0.1', $items['ip']['identifier']);
|
||||
$this->assertSame('staff', $items['account']['identifier']);
|
||||
|
||||
$this->actingAs($admin, 'admin')->deleteJson("/api/v1/administrator/login-blocks/{$id}")->assertOk();
|
||||
$this->actingAs($admin, 'admin')->deleteJson("/api/v1/administrator/login-blocks/{$items['ip']['id']}")->assertOk();
|
||||
$this->app['auth']->forgetGuards();
|
||||
$this->login('staff', 'correct-password-123')->assertStatus(429);
|
||||
|
||||
$this->actingAs($admin, 'admin')->deleteJson("/api/v1/administrator/login-blocks/{$items['account']['id']}")->assertOk();
|
||||
$this->app['auth']->forgetGuards();
|
||||
$this->login('staff', 'correct-password-123')->assertOk();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user