Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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"
Expand Down
38 changes: 36 additions & 2 deletions src/AuthenticationService.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -36,8 +39,9 @@
/**
* Authentication Service
*/
class AuthenticationService implements AuthenticationServiceInterface, ImpersonationInterface
class AuthenticationService implements AuthenticationServiceInterface, ImpersonationInterface, EventDispatcherInterface
{
use EventDispatcherTrait;
use InstanceConfigTrait;

/**
Expand Down Expand Up @@ -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(
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it ok if this trigger on every request? Perhaps we should include that in the documentation?

Copy link
Copy Markdown
Member Author

@ADmad ADmad Jul 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm.. I overlooked the fact that this would trigger even when reading the user record from session. For the use cases I mentioned, the event is only needed for a login form POST. So triggering an event only from FormAuthenticate would make more sense.

$request,
$authenticator,
$authenticator->authenticate($request),
);

if ($result->isValid()) {
$this->_successfulAuthenticator = $authenticator;

Expand All @@ -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
*
Expand Down
109 changes: 109 additions & 0 deletions src/Event/AuthenticateEvent.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
<?php
declare(strict_types=1);

/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 3.4.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Authentication\Event;

use Authentication\AuthenticationServiceInterface;
use Authentication\Authenticator\AuthenticatorInterface;
use Authentication\Authenticator\ResultInterface;
use Cake\Event\Event;
use InvalidArgumentException;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ServerRequestInterface;

/**
* Event triggered when authentication is run.
*
* @extends \Cake\Event\Event<\Authentication\AuthenticationServiceInterface>
*/
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'];
}
}
65 changes: 65 additions & 0 deletions tests/TestCase/AuthenticationServiceTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
*
Expand Down
6 changes: 6 additions & 0 deletions tests/bootstrap.php
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,12 @@
],
]);

Cache::setConfig([
'_cake_core_' => [
'engine' => 'Array',
],
]);

if (!getenv('DB_URL')) {
putenv('DB_URL=sqlite:///:memory:');
}
Expand Down