feat(security): emit authentication failures for firewall handling
Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
This commit is contained in:
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace KTXC\Http\Request;
|
||||
|
||||
/**
|
||||
* Holds the HTTP request for the duration of the current runtime execution.
|
||||
*/
|
||||
final class RequestContext
|
||||
{
|
||||
private ?Request $request = null;
|
||||
|
||||
public function initialize(Request $request): void
|
||||
{
|
||||
if ($this->request !== null) {
|
||||
throw new \LogicException('The request context has already been initialized.');
|
||||
}
|
||||
|
||||
$this->request = $request;
|
||||
}
|
||||
|
||||
public function current(): ?Request
|
||||
{
|
||||
return $this->request;
|
||||
}
|
||||
|
||||
public function clear(): void
|
||||
{
|
||||
$this->request = null;
|
||||
}
|
||||
}
|
||||
@@ -38,6 +38,7 @@ class Module extends ModuleInstanceAbstract implements ModuleConsoleInterface, M
|
||||
AuthenticationFailedEvent::class,
|
||||
FirewallService::class,
|
||||
'handleAuthFailure',
|
||||
DeliveryMode::Immediate,
|
||||
priority: 100,
|
||||
);
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ use KTXC\Http\Middleware\MiddlewarePipeline;
|
||||
use KTXC\Http\Middleware\RouterMiddleware;
|
||||
use KTXC\Http\Middleware\TenantMiddleware;
|
||||
use KTXC\Http\Request\Request;
|
||||
use KTXC\Http\Request\RequestContext;
|
||||
use KTXC\Http\Response\Response;
|
||||
use KTXC\KernelInterface;
|
||||
|
||||
@@ -25,26 +26,34 @@ final class HttpRuntime
|
||||
public function run(?Request $request = null, bool $send = true): Response
|
||||
{
|
||||
$request ??= Request::createFromGlobals();
|
||||
$requestContext = null;
|
||||
|
||||
return $this->kernel->executionRunner()->execute(
|
||||
ExecutionDescriptor::http(),
|
||||
function () use ($request, $send): Response {
|
||||
$response = $this->pipeline()->handle($request);
|
||||
if ($send) {
|
||||
$response->send();
|
||||
}
|
||||
try {
|
||||
return $this->kernel->executionRunner()->execute(
|
||||
ExecutionDescriptor::http(),
|
||||
function () use ($request, $send, &$requestContext): Response {
|
||||
$requestContext = $this->kernel->container()->get(RequestContext::class);
|
||||
$requestContext->initialize($request);
|
||||
|
||||
return $response;
|
||||
},
|
||||
function (\Throwable $error) use ($send): Response {
|
||||
$response = $this->errorResponse($error);
|
||||
if ($send) {
|
||||
$response->send();
|
||||
}
|
||||
$response = $this->pipeline()->handle($request);
|
||||
if ($send) {
|
||||
$response->send();
|
||||
}
|
||||
|
||||
return $response;
|
||||
},
|
||||
);
|
||||
return $response;
|
||||
},
|
||||
function (\Throwable $error) use ($send): Response {
|
||||
$response = $this->errorResponse($error);
|
||||
if ($send) {
|
||||
$response->send();
|
||||
}
|
||||
|
||||
return $response;
|
||||
},
|
||||
);
|
||||
} finally {
|
||||
$requestContext?->clear();
|
||||
}
|
||||
}
|
||||
|
||||
private function pipeline(): MiddlewarePipeline
|
||||
|
||||
@@ -8,11 +8,13 @@ use KTXC\Models\Identity\User;
|
||||
use KTXC\Resource\ProviderManager;
|
||||
use KTXC\Security\Authentication\AuthenticationRequest;
|
||||
use KTXC\Security\Authentication\AuthenticationResponse;
|
||||
use KTXC\Security\Event\AuthenticationFailedEvent;
|
||||
use KTXC\Service\TokenService;
|
||||
use KTXC\Service\UserAccountsService;
|
||||
use KTXC\Context\TenantContextInterface;
|
||||
use KTXF\Cache\CacheScope;
|
||||
use KTXF\Cache\EphemeralCacheInterface;
|
||||
use KTXF\Event\EventDispatcherInterface;
|
||||
use KTXF\Security\Authentication\AuthenticationProviderInterface;
|
||||
use KTXF\Security\Authentication\AuthenticationSession;
|
||||
use KTXF\Security\Authentication\ProviderContext;
|
||||
@@ -31,6 +33,7 @@ class AuthenticationManager
|
||||
private readonly ProviderManager $providerManager,
|
||||
private readonly TokenService $tokenService,
|
||||
private readonly UserAccountsService $userService,
|
||||
private readonly EventDispatcherInterface $events,
|
||||
) {
|
||||
$this->securityCode = $this->tenantContext->configuration()->security()->code();
|
||||
}
|
||||
@@ -187,6 +190,10 @@ class AuthenticationManager
|
||||
|
||||
if (!$result->isSuccess()) {
|
||||
$this->saveSession($session);
|
||||
$this->publishAuthenticationFailure(
|
||||
$session,
|
||||
$result->errorCode ?? AuthenticationResponse::ERROR_INVALID_CREDENTIALS,
|
||||
);
|
||||
return AuthenticationResponse::failed(
|
||||
AuthenticationResponse::ERROR_INVALID_CREDENTIALS,
|
||||
'Authentication failed. If you haven\'t set up this method, try another option.',
|
||||
@@ -389,6 +396,10 @@ class AuthenticationManager
|
||||
$result = $provider->completeRedirect($context, $request->params);
|
||||
|
||||
if ($result->isFailed()) {
|
||||
$this->publishAuthenticationFailure(
|
||||
$session,
|
||||
$result->errorCode ?? AuthenticationResponse::ERROR_INVALID_CREDENTIALS,
|
||||
);
|
||||
$this->deleteSession($session->id);
|
||||
return AuthenticationResponse::failed(
|
||||
AuthenticationResponse::ERROR_INVALID_CREDENTIALS,
|
||||
@@ -566,6 +577,17 @@ class AuthenticationManager
|
||||
// Helper Methods
|
||||
// =========================================================================
|
||||
|
||||
private function publishAuthenticationFailure(
|
||||
AuthenticationSession $session,
|
||||
string $reason,
|
||||
): void {
|
||||
$this->events->dispatch(new AuthenticationFailedEvent(
|
||||
userId: $session->userIdentifier,
|
||||
reason: $reason,
|
||||
tenantId: $session->tenantIdentifier,
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Build provider context from session
|
||||
*/
|
||||
|
||||
@@ -9,15 +9,10 @@ use KTXF\Event\Event;
|
||||
final class AuthenticationFailedEvent extends Event implements SecurityEventInterface
|
||||
{
|
||||
public function __construct(
|
||||
private readonly string $ipAddress,
|
||||
private readonly ?string $deviceFingerprint = null,
|
||||
private readonly ?string $userId = null,
|
||||
private readonly ?string $reason = null,
|
||||
?string $tenantId = null,
|
||||
?string $identityId = null,
|
||||
private readonly ?string $userAgent = null,
|
||||
private readonly ?string $requestPath = null,
|
||||
private readonly ?string $requestMethod = null,
|
||||
) {
|
||||
parent::__construct(
|
||||
self::class,
|
||||
@@ -27,31 +22,6 @@ final class AuthenticationFailedEvent extends Event implements SecurityEventInte
|
||||
);
|
||||
}
|
||||
|
||||
public function getIpAddress(): string
|
||||
{
|
||||
return $this->ipAddress;
|
||||
}
|
||||
|
||||
public function getDeviceFingerprint(): ?string
|
||||
{
|
||||
return $this->deviceFingerprint;
|
||||
}
|
||||
|
||||
public function getUserAgent(): ?string
|
||||
{
|
||||
return $this->userAgent;
|
||||
}
|
||||
|
||||
public function getRequestPath(): ?string
|
||||
{
|
||||
return $this->requestPath;
|
||||
}
|
||||
|
||||
public function getRequestMethod(): ?string
|
||||
{
|
||||
return $this->requestMethod;
|
||||
}
|
||||
|
||||
public function getUserId(): ?string
|
||||
{
|
||||
return $this->userId;
|
||||
|
||||
@@ -9,7 +9,7 @@ use KTXF\Event\Event;
|
||||
/**
|
||||
* Security-specific event for authentication and access control events
|
||||
*/
|
||||
final class SecurityEvent extends Event implements SecurityEventInterface
|
||||
final class SecurityEvent extends Event implements SecurityRequestEventInterface
|
||||
{
|
||||
// Event names
|
||||
public const AUTH_SUCCESS = 'security.auth.success';
|
||||
|
||||
@@ -18,16 +18,6 @@ interface SecurityEventInterface
|
||||
|
||||
public function getIdentityId(): ?string;
|
||||
|
||||
public function getIpAddress(): ?string;
|
||||
|
||||
public function getDeviceFingerprint(): ?string;
|
||||
|
||||
public function getUserAgent(): ?string;
|
||||
|
||||
public function getRequestPath(): ?string;
|
||||
|
||||
public function getRequestMethod(): ?string;
|
||||
|
||||
public function getUserId(): ?string;
|
||||
|
||||
public function getReason(): ?string;
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace KTXC\Security\Event;
|
||||
|
||||
interface SecurityRequestEventInterface extends SecurityEventInterface
|
||||
{
|
||||
public function getIpAddress(): ?string;
|
||||
|
||||
public function getDeviceFingerprint(): ?string;
|
||||
|
||||
public function getUserAgent(): ?string;
|
||||
|
||||
public function getRequestPath(): ?string;
|
||||
|
||||
public function getRequestMethod(): ?string;
|
||||
}
|
||||
@@ -5,6 +5,7 @@ declare(strict_types=1);
|
||||
namespace KTXC\Service;
|
||||
|
||||
use KTXC\Http\Request\Request;
|
||||
use KTXC\Http\Request\RequestContext;
|
||||
use KTXC\Models\Firewall\FirewallRuleObject;
|
||||
use KTXC\Models\Firewall\FirewallLogObject;
|
||||
use KTXC\Stores\FirewallStore;
|
||||
@@ -12,6 +13,7 @@ use KTXC\Context\TenantContextInterface;
|
||||
use KTXC\Security\Event\AuthenticationFailedEvent;
|
||||
use KTXC\Security\Event\SecurityEvent;
|
||||
use KTXC\Security\Event\SecurityEventInterface;
|
||||
use KTXC\Security\Event\SecurityRequestEventInterface;
|
||||
use KTXF\Event\EventDispatcherInterface;
|
||||
use KTXF\IpUtils;
|
||||
|
||||
@@ -47,6 +49,7 @@ class FirewallService
|
||||
private readonly EventDispatcherInterface $events,
|
||||
private readonly FirewallRuleManager $rules,
|
||||
private readonly FirewallRuleCache $ruleCache,
|
||||
private readonly RequestContext $requestContext,
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -134,14 +137,15 @@ class FirewallService
|
||||
*/
|
||||
public function handleAuthFailure(AuthenticationFailedEvent $event): void
|
||||
{
|
||||
$ipAddress = $event->getIpAddress();
|
||||
$request = $this->requestContext->current();
|
||||
$ipAddress = $request?->getClientIp();
|
||||
$tenantId = $event->getTenantId() ?? $this->tenantContext->identifier();
|
||||
|
||||
if (!$ipAddress || !$tenantId) {
|
||||
return;
|
||||
}
|
||||
|
||||
$log = $this->securityLog($event);
|
||||
$log = $this->securityLog($event, $request);
|
||||
if ($log === null || !$this->store->createLogOnce($log)) {
|
||||
return;
|
||||
}
|
||||
@@ -235,7 +239,10 @@ class FirewallService
|
||||
}
|
||||
}
|
||||
|
||||
private function securityLog(SecurityEventInterface $event): ?FirewallLogObject
|
||||
private function securityLog(
|
||||
SecurityEventInterface $event,
|
||||
?Request $request = null,
|
||||
): ?FirewallLogObject
|
||||
{
|
||||
$tenantId = $event->getTenantId() ?? $this->tenantContext->identifier();
|
||||
$ruleScope = $event->get('ruleScope');
|
||||
@@ -243,14 +250,19 @@ class FirewallService
|
||||
return null;
|
||||
}
|
||||
|
||||
$requestEvent = $event instanceof SecurityRequestEventInterface ? $event : null;
|
||||
|
||||
$log = new FirewallLogObject();
|
||||
return $log->setEventId($event->getEventId())
|
||||
->setTenantId($tenantId)
|
||||
->setIpAddress($event->getIpAddress())
|
||||
->setDeviceFingerprint($event->getDeviceFingerprint())
|
||||
->setUserAgent($event->getUserAgent())
|
||||
->setRequestPath($event->getRequestPath())
|
||||
->setRequestMethod($event->getRequestMethod())
|
||||
->setIpAddress($request?->getClientIp() ?? $requestEvent?->getIpAddress())
|
||||
->setDeviceFingerprint(
|
||||
$request?->headers->get('X-Device-Fingerprint')
|
||||
?? $requestEvent?->getDeviceFingerprint()
|
||||
)
|
||||
->setUserAgent($request?->headers->get('User-Agent') ?? $requestEvent?->getUserAgent())
|
||||
->setRequestPath($request?->getPathInfo() ?? $requestEvent?->getRequestPath())
|
||||
->setRequestMethod($request?->getMethod() ?? $requestEvent?->getRequestMethod())
|
||||
->setEventType($this->mapEventToLogType($event->getName()))
|
||||
->setResult($this->mapEventToResult($event))
|
||||
->setRuleId($event->get('ruleId'))
|
||||
|
||||
Reference in New Issue
Block a user