feat(security): emit authentication failures for firewall handling
Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
This commit is contained in:
@@ -5,6 +5,8 @@ declare(strict_types=1);
|
||||
namespace KTXT\Unit\Service;
|
||||
|
||||
use KTXC\Context\TenantContextInterface;
|
||||
use KTXC\Http\Request\Request;
|
||||
use KTXC\Http\Request\RequestContext;
|
||||
use KTXC\Models\Firewall\FirewallRuleObject;
|
||||
use KTXC\Models\Firewall\FirewallLogObject;
|
||||
use KTXC\Models\Tenant\TenantConfiguration;
|
||||
@@ -25,6 +27,7 @@ class FirewallServiceTest extends TestCase
|
||||
private FirewallStore&MockObject $store;
|
||||
private TenantContextInterface&MockObject $tenantContext;
|
||||
private EventDispatcherInterface&MockObject $events;
|
||||
private RequestContext $requestContext;
|
||||
private FirewallService $service;
|
||||
private ?string $currentTenant;
|
||||
private ?TenantConfiguration $currentConfiguration;
|
||||
@@ -34,6 +37,16 @@ class FirewallServiceTest extends TestCase
|
||||
$this->store = $this->createMock(FirewallStore::class);
|
||||
$this->tenantContext = $this->createMock(TenantContextInterface::class);
|
||||
$this->events = $this->createMock(EventDispatcherInterface::class);
|
||||
$this->requestContext = new RequestContext();
|
||||
$this->requestContext->initialize(Request::create(
|
||||
'/login',
|
||||
'POST',
|
||||
server: [
|
||||
'REMOTE_ADDR' => '203.0.113.10',
|
||||
'HTTP_USER_AGENT' => 'Test Agent',
|
||||
'HTTP_X_DEVICE_FINGERPRINT' => 'device-a',
|
||||
],
|
||||
));
|
||||
$this->currentTenant = 'tenant-a';
|
||||
$this->currentConfiguration = null;
|
||||
$this->tenantContext->method('identifier')->willReturnCallback(
|
||||
@@ -49,7 +62,8 @@ class FirewallServiceTest extends TestCase
|
||||
$this->tenantContext,
|
||||
$this->events,
|
||||
$manager,
|
||||
$cache
|
||||
$cache,
|
||||
$this->requestContext,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -253,7 +267,7 @@ class FirewallServiceTest extends TestCase
|
||||
$this->store->expects($this->never())->method('createLog');
|
||||
|
||||
$this->service->logSecurityEvent(
|
||||
new AuthenticationFailedEvent('203.0.113.10')
|
||||
new AuthenticationFailedEvent()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -378,16 +392,53 @@ class FirewallServiceTest extends TestCase
|
||||
->willReturn(4);
|
||||
$this->events->expects($this->never())->method('dispatch');
|
||||
|
||||
$event = new AuthenticationFailedEvent(
|
||||
'203.0.113.10',
|
||||
tenantId: 'tenant-a',
|
||||
);
|
||||
$event = new AuthenticationFailedEvent(tenantId: 'tenant-a');
|
||||
$this->service->handleAuthFailure($event);
|
||||
|
||||
self::assertSame(8, $this->currentConfiguration->firewall()->maxAuthFailures());
|
||||
self::assertSame(7200, $this->currentConfiguration->firewall()->autoBlockDuration());
|
||||
}
|
||||
|
||||
#[TestDox('Authentication failures combine event facts with the current request')]
|
||||
public function testAuthenticationFailureRequestContext(): void
|
||||
{
|
||||
$this->store->expects($this->once())
|
||||
->method('createLogOnce')
|
||||
->with(self::callback(static function (FirewallLogObject $log): bool {
|
||||
return $log->getIpAddress() === '203.0.113.10'
|
||||
&& $log->getDeviceFingerprint() === 'device-a'
|
||||
&& $log->getUserAgent() === 'Test Agent'
|
||||
&& $log->getRequestPath() === '/login'
|
||||
&& $log->getRequestMethod() === 'POST'
|
||||
&& $log->getIdentityId() === 'user-a'
|
||||
&& $log->getMetadata()['reason'] === 'invalid_credentials';
|
||||
}))
|
||||
->willReturn(true);
|
||||
$this->store->expects($this->once())
|
||||
->method('countRecentFailures')
|
||||
->with('tenant-a', '203.0.113.10', 300)
|
||||
->willReturn(0);
|
||||
|
||||
$this->service->handleAuthFailure(new AuthenticationFailedEvent(
|
||||
userId: 'user-a',
|
||||
reason: 'invalid_credentials',
|
||||
tenantId: 'tenant-a',
|
||||
));
|
||||
}
|
||||
|
||||
#[TestDox('Authentication failures outside HTTP request context are ignored')]
|
||||
public function testAuthenticationFailureWithoutRequestContext(): void
|
||||
{
|
||||
$this->requestContext->clear();
|
||||
$this->store->expects($this->never())->method('createLogOnce');
|
||||
$this->store->expects($this->never())->method('countRecentFailures');
|
||||
|
||||
$this->service->handleAuthFailure(new AuthenticationFailedEvent(
|
||||
reason: 'invalid_credentials',
|
||||
tenantId: 'tenant-a',
|
||||
));
|
||||
}
|
||||
|
||||
#[TestDox('Unsafe numeric firewall settings fall back to safe defaults')]
|
||||
public function testConfigurationBounds(): void
|
||||
{
|
||||
@@ -404,10 +455,7 @@ class FirewallServiceTest extends TestCase
|
||||
->with('tenant-a', '203.0.113.10', 300)
|
||||
->willReturn(0);
|
||||
|
||||
$event = new AuthenticationFailedEvent(
|
||||
'203.0.113.10',
|
||||
tenantId: 'tenant-a',
|
||||
);
|
||||
$event = new AuthenticationFailedEvent(tenantId: 'tenant-a');
|
||||
$this->service->handleAuthFailure($event);
|
||||
}
|
||||
|
||||
@@ -460,10 +508,7 @@ class FirewallServiceTest extends TestCase
|
||||
}
|
||||
});
|
||||
|
||||
$event = new AuthenticationFailedEvent(
|
||||
'203.0.113.10',
|
||||
tenantId: 'tenant-event',
|
||||
);
|
||||
$event = new AuthenticationFailedEvent(tenantId: 'tenant-event');
|
||||
$this->service->handleAuthFailure($event);
|
||||
|
||||
self::assertSame(['tenant-event', 'tenant-event', 'tenant-event'], $publishedTenants);
|
||||
@@ -486,7 +531,7 @@ class FirewallServiceTest extends TestCase
|
||||
$this->events->expects($this->never())->method('dispatch');
|
||||
|
||||
$this->service->handleAuthFailure(
|
||||
new AuthenticationFailedEvent('203.0.113.10')
|
||||
new AuthenticationFailedEvent()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -500,7 +545,7 @@ class FirewallServiceTest extends TestCase
|
||||
->willReturn(0);
|
||||
|
||||
$this->service->handleAuthFailure(
|
||||
new AuthenticationFailedEvent('203.0.113.10')
|
||||
new AuthenticationFailedEvent()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -512,7 +557,7 @@ class FirewallServiceTest extends TestCase
|
||||
$this->store->expects($this->never())->method('depositRule');
|
||||
|
||||
$this->service->handleAuthFailure(
|
||||
new AuthenticationFailedEvent('203.0.113.10')
|
||||
new AuthenticationFailedEvent()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -526,7 +571,7 @@ class FirewallServiceTest extends TestCase
|
||||
->method('countRecentFailures')
|
||||
->with('tenant-a', '203.0.113.10', 300)
|
||||
->willReturn(1);
|
||||
$event = new AuthenticationFailedEvent('203.0.113.10');
|
||||
$event = new AuthenticationFailedEvent();
|
||||
$eventId = $event->getEventId();
|
||||
|
||||
$this->service->handleAuthFailure($event);
|
||||
|
||||
Reference in New Issue
Block a user