You've already forked php-auth
generated from siteworxpro/Php-Template
Enhance user and audit logging by adding client ID to user scopes and login events
All checks were successful
🧪✨ Tests Workflow / 🧪 ✨ Database Migrations (push) Successful in -21s
🧪✨ Tests Workflow / 🛡️ 🔒 License Check (push) Successful in -22s
🧪✨ Tests Workflow / 🛡️ 🔒 Library Audit (push) Successful in -12s
🧪✨ Tests Workflow / 📝 ✨ Code Lint (push) Successful in -20s
🧪✨ Tests Workflow / 🐙 🔍 Code Sniffer (push) Successful in -14s
🧪✨ Tests Workflow / 🧪 ✅ Unit Tests (push) Successful in -36s
All checks were successful
🧪✨ Tests Workflow / 🧪 ✨ Database Migrations (push) Successful in -21s
🧪✨ Tests Workflow / 🛡️ 🔒 License Check (push) Successful in -22s
🧪✨ Tests Workflow / 🛡️ 🔒 Library Audit (push) Successful in -12s
🧪✨ Tests Workflow / 📝 ✨ Code Lint (push) Successful in -20s
🧪✨ Tests Workflow / 🐙 🔍 Code Sniffer (push) Successful in -14s
🧪✨ Tests Workflow / 🧪 ✅ Unit Tests (push) Successful in -36s
This commit is contained in:
@@ -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)
|
||||
);
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
41
src/EventListeners/AccessTokenIssuedListener.php
Normal file
41
src/EventListeners/AccessTokenIssuedListener.php
Normal file
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Siteworxpro\App\EventListeners;
|
||||
|
||||
use Siteworxpro\App\Attributes\Events\ListensFor;
|
||||
use Siteworxpro\App\Models\AuditLog;
|
||||
use Siteworxpro\App\Models\Enums\AuditLogAction;
|
||||
|
||||
#[ListensFor(\Siteworxpro\App\Events\AccessToken\Issued::class)]
|
||||
class AccessTokenIssuedListener extends Listener
|
||||
{
|
||||
/**
|
||||
* Handle the event.
|
||||
*
|
||||
* @param string | \Siteworxpro\App\Events\AccessToken\Issued $event
|
||||
* @param array $payload
|
||||
* @return AuditLog|null
|
||||
*/
|
||||
public function __invoke(mixed $event, array $payload = []): ?AuditLog
|
||||
{
|
||||
if (is_string($event)) {
|
||||
$event = $payload[0] ?? null;
|
||||
}
|
||||
|
||||
if (!$event instanceof \Siteworxpro\App\Events\AccessToken\Issued) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return AuditLog::create([
|
||||
'user_id' => null,
|
||||
'action' => AuditLogAction::TOKEN_ISSUED,
|
||||
'details' => [
|
||||
'response_status' => $event->getResponse()->getStatusCode(),
|
||||
'client_id' => $event->getClient()->getIdentifier(),
|
||||
'client_name' => $event->getClient()->getName(),
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
],
|
||||
]);
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user