From 11bf2a5efdb8ac33921cc47a4081a8de611124a9 Mon Sep 17 00:00:00 2001 From: ADmad Date: Mon, 14 Jul 2025 10:15:23 +0530 Subject: [PATCH 1/2] Dispatch a Authentication.authenticate event on successfull authentication. Subscribing to this event would allow the app to do to various like updating last login time, keep track of failed login attempts etc. --- composer.json | 3 +- src/AuthenticationService.php | 38 ++++++- src/Event/AuthenticateEvent.php | 109 +++++++++++++++++++ tests/TestCase/AuthenticationServiceTest.php | 65 +++++++++++ 4 files changed, 212 insertions(+), 3 deletions(-) create mode 100644 src/Event/AuthenticateEvent.php diff --git a/composer.json b/composer.json index 22782b3d..ef2587b6 100644 --- a/composer.json +++ b/composer.json @@ -24,6 +24,7 @@ }, "require": { "php": ">=8.1", + "cakephp/event":"^5.0", "cakephp/http": "^5.0", "laminas/laminas-diactoros": "^3.0", "psr/http-client": "^1.0", @@ -32,7 +33,7 @@ "psr/http-server-middleware": "^1.0" }, "require-dev": { - "cakephp/cakephp": "^5.1.0", + "cakephp/cakephp": "^5.0", "cakephp/cakephp-codesniffer": "^5.0", "firebase/php-jwt": "^6.2", "phpunit/phpunit": "^10.5.5 || ^11.1.3 || ^12.0.9" diff --git a/src/AuthenticationService.php b/src/AuthenticationService.php index e4a08276..74638c60 100644 --- a/src/AuthenticationService.php +++ b/src/AuthenticationService.php @@ -23,9 +23,12 @@ use Authentication\Authenticator\PersistenceInterface; use Authentication\Authenticator\ResultInterface; use Authentication\Authenticator\StatelessInterface; +use Authentication\Event\AuthenticateEvent; use Authentication\Identifier\IdentifierCollection; use Authentication\Identifier\IdentifierInterface; use Cake\Core\InstanceConfigTrait; +use Cake\Event\EventDispatcherInterface; +use Cake\Event\EventDispatcherTrait; use Cake\Routing\Router; use InvalidArgumentException; use Psr\Http\Message\ResponseInterface; @@ -36,8 +39,9 @@ /** * Authentication Service */ -class AuthenticationService implements AuthenticationServiceInterface, ImpersonationInterface +class AuthenticationService implements AuthenticationServiceInterface, ImpersonationInterface, EventDispatcherInterface { + use EventDispatcherTrait; use InstanceConfigTrait; /** @@ -190,7 +194,12 @@ public function authenticate(ServerRequestInterface $request): ResultInterface $result = null; /** @var \Authentication\Authenticator\AuthenticatorInterface $authenticator */ foreach ($this->authenticators() as $authenticator) { - $result = $authenticator->authenticate($request); + $result = $this->dispatchAuthenticateEvent( + $request, + $authenticator, + $authenticator->authenticate($request), + ); + if ($result->isValid()) { $this->_successfulAuthenticator = $authenticator; @@ -213,6 +222,31 @@ public function authenticate(ServerRequestInterface $request): ResultInterface return $this->_result = $result; } + /** + * Dispatches an authenticate event. + * + * @param \Psr\Http\Message\ServerRequestInterface $request The request. + * @param \Authentication\Authenticator\AuthenticatorInterface $authenticator The authenticator instance. + * @param \Authentication\Authenticator\ResultInterface $result The authentication result. + * @return \Authentication\Authenticator\ResultInterface + */ + protected function dispatchAuthenticateEvent( + ServerRequestInterface $request, + AuthenticatorInterface $authenticator, + ResultInterface $result, + ): ResultInterface { + /** @var \Authentication\Event\AuthenticateEvent $event */ + $event = $this->getEventManager()->dispatch(new AuthenticateEvent( + AuthenticateEvent::NAME, + $this, + $request, + $authenticator, + $result, + )); + + return $event->getResult(); + } + /** * Clears the identity from authenticators that store them and the request * diff --git a/src/Event/AuthenticateEvent.php b/src/Event/AuthenticateEvent.php new file mode 100644 index 00000000..cd21d8ee --- /dev/null +++ b/src/Event/AuthenticateEvent.php @@ -0,0 +1,109 @@ + + */ +class AuthenticateEvent extends Event +{ + /** + * Event name + * + * @var string + */ + public const NAME = 'Authentication.authenticate'; + + /** + * Constructor + * + * @param string $name Name of the event + * @param \Authentication\AuthenticationServiceInterface $subject The Authentication service instance this event applies to. + * @param \Psr\Http\Message\ServerRequestInterface $request The request instance. + * @param \Authentication\Authenticator\AuthenticatorInterface $authenticator The authenticator instance. + * @param \Authentication\Authenticator\ResultInterface $result The authentication result. + */ + public function __construct( + string $name, + AuthenticationServiceInterface $subject, + ServerRequestInterface $request, + AuthenticatorInterface $authenticator, + ResultInterface $result, + ) { + $this->result = $result; + + parent::__construct($name, $subject, compact('request', 'authenticator')); + } + + /** + * The authentication result. + * + * @return \Authentication\Authenticator\ResultInterface + */ + public function getResult(): ResultInterface + { + return $this->result; + } + + /** + * Set the authentication result. + * + * @param \Authentication\Authenticator\ResultInterface|null $value The result to set. + * @return $this + */ + public function setResult(mixed $value = null) + { + if (!$value instanceof ResultInterface) { + throw new InvalidArgumentException( + 'The result for Authentication.authenticate event must be a ' + . '`Authentication\Authenticator\ResultInterface` instance.', + ); + } + + return parent::setResult($value); + } + + /** + * Get the request instance. + * + * @return \Psr\Http\Message\RequestInterface + */ + public function getRequest(): RequestInterface + { + return $this->_data['request']; + } + + /** + * Get the adapter options. + * + * @return \Authentication\Authenticator\AuthenticatorInterface + */ + public function getAuthenticator(): AuthenticatorInterface + { + return $this->_data['authenticator']; + } +} diff --git a/tests/TestCase/AuthenticationServiceTest.php b/tests/TestCase/AuthenticationServiceTest.php index a551e543..93933727 100644 --- a/tests/TestCase/AuthenticationServiceTest.php +++ b/tests/TestCase/AuthenticationServiceTest.php @@ -22,11 +22,13 @@ use Authentication\Authenticator\AuthenticatorInterface; use Authentication\Authenticator\FormAuthenticator; use Authentication\Authenticator\Result; +use Authentication\Event\AuthenticateEvent; use Authentication\Identifier\IdentifierCollection; use Authentication\Identifier\PasswordIdentifier; use Authentication\Identity; use Authentication\IdentityInterface; use Authentication\Test\TestCase\AuthenticationTestCase as TestCase; +use Cake\Event\EventManager; use Cake\Http\Exception\UnauthorizedException; use Cake\Http\Response; use Cake\Http\ServerRequest; @@ -106,6 +108,69 @@ public function testAuthenticate() $this->assertInstanceOf(PasswordIdentifier::class, $identifier); } + public function testAuthenticateSuccessEvent(): void + { + $request = ServerRequestFactory::fromGlobals( + ['REQUEST_URI' => '/testpath'], + [], + ['username' => 'mariano', 'password' => 'password'], + ); + + $service = new AuthenticationService([ + 'authenticators' => [ + 'Authentication.Form' => [ + 'identifier' => 'Authentication.Password', + ], + ], + ]); + + EventManager::instance()->on( + 'Authentication.authenticate', + function (AuthenticateEvent $event): void { + $event->setResult(new Result( + ['user' => 'admad'], + Result::SUCCESS, + )); + }, + ); + + $result = $service->authenticate($request); + $this->assertTrue($result->isValid()); + $this->assertSame(['user' => 'admad'], $result->getData()); + } + + public function testAuthenticateFailureEvent(): void + { + $request = ServerRequestFactory::fromGlobals( + ['REQUEST_URI' => '/testpath'], + [], + ['username' => 'mariano', 'password' => ''], + ); + + $service = new AuthenticationService([ + 'authenticators' => [ + 'Authentication.Form' => [ + 'identifier' => 'Authentication.Password', + ], + ], + ]); + + EventManager::instance()->on( + 'Authentication.authenticate', + function (AuthenticateEvent $event): void { + $event->setResult(new Result( + ['user' => 'admad'], + Result::FAILURE_OTHER, + )); + }, + ); + + $result = $service->authenticate($request); + $this->assertFalse($result->isValid()); + $this->assertSame(Result::FAILURE_OTHER, $result->getStatus()); + $this->assertSame(['user' => 'admad'], $result->getData()); + } + /** * test authenticate() with a challenger authenticator * From bae0815ca6a87ac9c057e8fe8be3effa2ba5d703 Mon Sep 17 00:00:00 2001 From: ADmad Date: Mon, 21 Jul 2025 13:20:41 +0530 Subject: [PATCH 2/2] Fix tests for Cake 5.0 --- tests/bootstrap.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/bootstrap.php b/tests/bootstrap.php index b55f16b5..4d2b0aaf 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -61,6 +61,12 @@ ], ]); +Cache::setConfig([ + '_cake_core_' => [ + 'engine' => 'Array', + ], +]); + if (!getenv('DB_URL')) { putenv('DB_URL=sqlite:///:memory:'); }