From 5ec683890ec8a156110974ee80c17e0ae61a3180 Mon Sep 17 00:00:00 2001 From: Ron Rise Date: Thu, 29 Jan 2026 23:45:23 -0500 Subject: [PATCH] Enhance user and audit logging by adding client ID to user scopes and login events --- .../000001_create_users_table.up.sql | 6 ++- .../000002_create_audit_table.up.sql | 2 +- src/Controllers/AccessTokenController.php | 2 +- src/Controllers/AuthorizeController.php | 7 +-- .../AccessTokenIssuedListener.php | 41 +++++++++++++++++ .../{LoginFailed.php => LoginListener.php} | 18 ++++++-- src/Events/AccessToken/Issued.php | 8 +++- src/Events/Login/LoginAttempt.php | 46 +++++++++++++++++-- src/Events/Login/LoginFailed.php | 40 +--------------- src/Events/Login/LoginSuccess.php | 17 +------ src/Models/Enums/AuditLogAction.php | 2 + 11 files changed, 118 insertions(+), 71 deletions(-) create mode 100644 src/EventListeners/AccessTokenIssuedListener.php rename src/EventListeners/{LoginFailed.php => LoginListener.php} (56%) diff --git a/db/migrations/000001_create_users_table.up.sql b/db/migrations/000001_create_users_table.up.sql index 1bb4474..26270b4 100644 --- a/db/migrations/000001_create_users_table.up.sql +++ b/db/migrations/000001_create_users_table.up.sql @@ -105,6 +105,10 @@ create table user_scopes constraint user_scopes_scope_id_fk references scopes on delete cascade, + client_id VARCHAR(26) not null + constraint user_scopes_client_id_fk + references clients + on delete cascade, constraint user_scopes_user_id_scope_id_key - unique (user_id, scope_id) + unique (user_id, scope_id, client_id) ); \ No newline at end of file diff --git a/db/migrations/000002_create_audit_table.up.sql b/db/migrations/000002_create_audit_table.up.sql index d75ea81..08c2e09 100644 --- a/db/migrations/000002_create_audit_table.up.sql +++ b/db/migrations/000002_create_audit_table.up.sql @@ -3,7 +3,7 @@ create table audit_logs id VARCHAR(26) not null constraint audit_logs_pkey primary key, - user_id integer, + user_id varchar(26) default null, action varchar(255) not null, timestamp timestamptz default current_timestamp, details jsonb diff --git a/src/Controllers/AccessTokenController.php b/src/Controllers/AccessTokenController.php index 034d077..66d4ff7 100644 --- a/src/Controllers/AccessTokenController.php +++ b/src/Controllers/AccessTokenController.php @@ -45,7 +45,7 @@ final class AccessTokenController extends Controller JsonResponseFactory::createJsonResponse([]) ); - Dispatcher::push(new Issued($response)); + Dispatcher::push(new Issued($client, $response)); return $response; } catch (OAuthServerException $e) { diff --git a/src/Controllers/AuthorizeController.php b/src/Controllers/AuthorizeController.php index 696c8e5..6346cf1 100644 --- a/src/Controllers/AuthorizeController.php +++ b/src/Controllers/AuthorizeController.php @@ -12,7 +12,6 @@ use League\OAuth2\Server\RequestTypes\AuthorizationRequest; use Nyholm\Psr7\Response; use Nyholm\Psr7\ServerRequest; use Nyholm\Psr7\Stream; -use Siteworxpro\App\Events\Login\LoginAttempt; use Siteworxpro\App\Events\Login\LoginFailed; use Siteworxpro\App\Events\Login\LoginSuccess; use Siteworxpro\App\Helpers\Rand; @@ -35,8 +34,6 @@ final class AuthorizeController extends Controller */ public function post(ServerRequest $request): Response { - Dispatcher::push(new LoginAttempt($request)); - $s = $request->getCookieParams()['s'] ?? ''; $password = $request->getParsedBody()['password'] ?? ''; @@ -69,7 +66,7 @@ final class AuthorizeController extends Controller $user = $client->loginUser($email, $password); if (!$user) { - Dispatcher::push(new LoginFailed($request)); + Dispatcher::push(new LoginFailed($request, $client)); return JsonResponseFactory::createJsonResponse([ 'success' => false, @@ -84,7 +81,7 @@ final class AuthorizeController extends Controller Redis::del('session:' . $s); - Dispatcher::push(new LoginSuccess($request, $user)); + Dispatcher::push(new LoginSuccess($request, $client, $user)); return JsonResponseFactory::createJsonResponse([ 'success' => true, diff --git a/src/EventListeners/AccessTokenIssuedListener.php b/src/EventListeners/AccessTokenIssuedListener.php new file mode 100644 index 0000000..b633b77 --- /dev/null +++ b/src/EventListeners/AccessTokenIssuedListener.php @@ -0,0 +1,41 @@ + null, + 'action' => AuditLogAction::TOKEN_ISSUED, + 'details' => [ + 'response_status' => $event->getResponse()->getStatusCode(), + 'client_id' => $event->getClient()->getIdentifier(), + 'client_name' => $event->getClient()->getName(), + ], + ]); + } +} diff --git a/src/EventListeners/LoginFailed.php b/src/EventListeners/LoginListener.php similarity index 56% rename from src/EventListeners/LoginFailed.php rename to src/EventListeners/LoginListener.php index b03e1f7..71d3876 100644 --- a/src/EventListeners/LoginFailed.php +++ b/src/EventListeners/LoginListener.php @@ -9,7 +9,8 @@ use Siteworxpro\App\Models\AuditLog; use Siteworxpro\App\Models\Enums\AuditLogAction; #[ListensFor(\Siteworxpro\App\Events\Login\LoginFailed::class)] -class LoginFailed extends Listener +#[ListensFor(\Siteworxpro\App\Events\Login\LoginSuccess::class)] +class LoginListener extends Listener { /** * Handle the event. @@ -24,15 +25,24 @@ class LoginFailed extends Listener $event = $payload[0] ?? null; } - if (!$event instanceof \Siteworxpro\App\Events\Login\LoginFailed) { + if ( + !$event instanceof \Siteworxpro\App\Events\Login\LoginFailed + && !$event instanceof \Siteworxpro\App\Events\Login\LoginSuccess + ) { return null; } + $action = $event instanceof \Siteworxpro\App\Events\Login\LoginSuccess + ? AuditLogAction::LOGIN_SUCCESS + : AuditLogAction::LOGIN_FAIL; + return AuditLog::create([ 'user_id' => $event->getUser()?->id, - 'action' => AuditLogAction::LOGIN_FAIL, + 'action' => $action, 'details' => [ - 'username_attempted' => $event->getUsernameAttempted(), + 'client_id' => $event->getClient()->client_id, + 'client_name' => $event->getClient()->name, + 'username' => $event->getUsernameAttempted(), 'ip_address' => $event->getRequestIp(), ], ]); diff --git a/src/Events/AccessToken/Issued.php b/src/Events/AccessToken/Issued.php index 57bc6bc..153aaad 100644 --- a/src/Events/AccessToken/Issued.php +++ b/src/Events/AccessToken/Issued.php @@ -5,10 +5,11 @@ declare(strict_types=1); namespace Siteworxpro\App\Events\AccessToken; use Nyholm\Psr7\Response; +use Siteworxpro\App\OAuth\Entities\Client; readonly class Issued { - public function __construct(private Response $response) + public function __construct(private Client $client, private Response $response) { } @@ -16,4 +17,9 @@ readonly class Issued { return $this->response; } + + public function getClient(): Client + { + return $this->client; + } } diff --git a/src/Events/Login/LoginAttempt.php b/src/Events/Login/LoginAttempt.php index b739a0c..083e022 100644 --- a/src/Events/Login/LoginAttempt.php +++ b/src/Events/Login/LoginAttempt.php @@ -5,16 +5,56 @@ declare(strict_types=1); namespace Siteworxpro\App\Events\Login; use Nyholm\Psr7\ServerRequest; +use Siteworxpro\App\Models\User; +use Siteworxpro\App\OAuth\Entities\Client; -readonly class LoginAttempt +abstract class LoginAttempt { public function __construct( - private ServerRequest $request, + private readonly ServerRequest $request, + private readonly Client $client, + private readonly ?User $user = null, ) { } - public function getEmail(): string + public function getRequestIp(): string + { + if ($this->request->getHeader('X-Forwarded-For')) { + $ipAddresses = explode(',', $this->request->getHeaderLine('X-Forwarded-For')); + return trim($ipAddresses[0]); + } + + if ($this->request->getHeader('X-Real-IP')) { + return $this->request->getHeaderLine('X-Real-IP'); + } + + if ($this->request->getServerParams()['HTTP_CLIENT_IP'] ?? false) { + return $this->request->getServerParams()['HTTP_CLIENT_IP']; + } + + if ($this->request->getServerParams()['HTTP_X_FORWARDED_FOR'] ?? false) { + $ipAddresses = explode(',', $this->request->getServerParams()['HTTP_X_FORWARDED_FOR']); + return trim($ipAddresses[0]); + } + + return $this->request->getServerParams()['REMOTE_ADDR'] ?? 'unknown'; + } + + public function getUsernameAttempted(): string { return $this->request->getParsedBody()['email'] ?? ''; } + + public function getUser(): ?User + { + return $this->user; + } + + /** + * @return Client + */ + public function getClient(): Client + { + return $this->client; + } } diff --git a/src/Events/Login/LoginFailed.php b/src/Events/Login/LoginFailed.php index 83eb4b6..25fbf46 100644 --- a/src/Events/Login/LoginFailed.php +++ b/src/Events/Login/LoginFailed.php @@ -7,44 +7,6 @@ namespace Siteworxpro\App\Events\Login; use Nyholm\Psr7\ServerRequest; use Siteworxpro\App\Models\User; -readonly class LoginFailed +class LoginFailed extends LoginAttempt { - public function __construct( - private ServerRequest $request, - private ?User $user = null, - ) { - } - - public function getRequestIp(): string - { - if ($this->request->getHeader('X-Forwarded-For')) { - $ipAddresses = explode(',', $this->request->getHeaderLine('X-Forwarded-For')); - return trim($ipAddresses[0]); - } - - if ($this->request->getHeader('X-Real-IP')) { - return $this->request->getHeaderLine('X-Real-IP'); - } - - if ($this->request->getServerParams()['HTTP_CLIENT_IP'] ?? false) { - return $this->request->getServerParams()['HTTP_CLIENT_IP']; - } - - if ($this->request->getServerParams()['HTTP_X_FORWARDED_FOR'] ?? false) { - $ipAddresses = explode(',', $this->request->getServerParams()['HTTP_X_FORWARDED_FOR']); - return trim($ipAddresses[0]); - } - - return $this->request->getServerParams()['REMOTE_ADDR'] ?? 'unknown'; - } - - public function getUsernameAttempted(): string - { - return $this->request->getParsedBody()['email'] ?? ''; - } - - public function getUser(): ?User - { - return $this->user; - } } diff --git a/src/Events/Login/LoginSuccess.php b/src/Events/Login/LoginSuccess.php index a8821b2..1bb6e59 100644 --- a/src/Events/Login/LoginSuccess.php +++ b/src/Events/Login/LoginSuccess.php @@ -7,21 +7,6 @@ namespace Siteworxpro\App\Events\Login; use Nyholm\Psr7\ServerRequest; use Siteworxpro\App\Models\User; -readonly class LoginSuccess +class LoginSuccess extends LoginAttempt { - public function __construct( - private ServerRequest $serverRequest, - private User $user - ) { - } - - public function getRequest(): ServerRequest - { - return $this->serverRequest; - } - - public function getUser(): User - { - return $this->user; - } } diff --git a/src/Models/Enums/AuditLogAction.php b/src/Models/Enums/AuditLogAction.php index 0f93b4f..cc1fee3 100644 --- a/src/Models/Enums/AuditLogAction.php +++ b/src/Models/Enums/AuditLogAction.php @@ -9,4 +9,6 @@ enum AuditLogAction: string case LOGIN_SUCCESS = 'login_success'; case LOGIN_FAIL = 'login_fail'; case LOGOUT = 'logout'; + + case TOKEN_ISSUED = 'token_issued'; }