Skip to content

Commit 11bf2a5

Browse files
committed
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.
1 parent 058de14 commit 11bf2a5

File tree

4 files changed

+212
-3
lines changed

4 files changed

+212
-3
lines changed

composer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
},
2525
"require": {
2626
"php": ">=8.1",
27+
"cakephp/event":"^5.0",
2728
"cakephp/http": "^5.0",
2829
"laminas/laminas-diactoros": "^3.0",
2930
"psr/http-client": "^1.0",
@@ -32,7 +33,7 @@
3233
"psr/http-server-middleware": "^1.0"
3334
},
3435
"require-dev": {
35-
"cakephp/cakephp": "^5.1.0",
36+
"cakephp/cakephp": "^5.0",
3637
"cakephp/cakephp-codesniffer": "^5.0",
3738
"firebase/php-jwt": "^6.2",
3839
"phpunit/phpunit": "^10.5.5 || ^11.1.3 || ^12.0.9"

src/AuthenticationService.php

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,12 @@
2323
use Authentication\Authenticator\PersistenceInterface;
2424
use Authentication\Authenticator\ResultInterface;
2525
use Authentication\Authenticator\StatelessInterface;
26+
use Authentication\Event\AuthenticateEvent;
2627
use Authentication\Identifier\IdentifierCollection;
2728
use Authentication\Identifier\IdentifierInterface;
2829
use Cake\Core\InstanceConfigTrait;
30+
use Cake\Event\EventDispatcherInterface;
31+
use Cake\Event\EventDispatcherTrait;
2932
use Cake\Routing\Router;
3033
use InvalidArgumentException;
3134
use Psr\Http\Message\ResponseInterface;
@@ -36,8 +39,9 @@
3639
/**
3740
* Authentication Service
3841
*/
39-
class AuthenticationService implements AuthenticationServiceInterface, ImpersonationInterface
42+
class AuthenticationService implements AuthenticationServiceInterface, ImpersonationInterface, EventDispatcherInterface
4043
{
44+
use EventDispatcherTrait;
4145
use InstanceConfigTrait;
4246

4347
/**
@@ -190,7 +194,12 @@ public function authenticate(ServerRequestInterface $request): ResultInterface
190194
$result = null;
191195
/** @var \Authentication\Authenticator\AuthenticatorInterface $authenticator */
192196
foreach ($this->authenticators() as $authenticator) {
193-
$result = $authenticator->authenticate($request);
197+
$result = $this->dispatchAuthenticateEvent(
198+
$request,
199+
$authenticator,
200+
$authenticator->authenticate($request),
201+
);
202+
194203
if ($result->isValid()) {
195204
$this->_successfulAuthenticator = $authenticator;
196205

@@ -213,6 +222,31 @@ public function authenticate(ServerRequestInterface $request): ResultInterface
213222
return $this->_result = $result;
214223
}
215224

225+
/**
226+
* Dispatches an authenticate event.
227+
*
228+
* @param \Psr\Http\Message\ServerRequestInterface $request The request.
229+
* @param \Authentication\Authenticator\AuthenticatorInterface $authenticator The authenticator instance.
230+
* @param \Authentication\Authenticator\ResultInterface $result The authentication result.
231+
* @return \Authentication\Authenticator\ResultInterface
232+
*/
233+
protected function dispatchAuthenticateEvent(
234+
ServerRequestInterface $request,
235+
AuthenticatorInterface $authenticator,
236+
ResultInterface $result,
237+
): ResultInterface {
238+
/** @var \Authentication\Event\AuthenticateEvent $event */
239+
$event = $this->getEventManager()->dispatch(new AuthenticateEvent(
240+
AuthenticateEvent::NAME,
241+
$this,
242+
$request,
243+
$authenticator,
244+
$result,
245+
));
246+
247+
return $event->getResult();
248+
}
249+
216250
/**
217251
* Clears the identity from authenticators that store them and the request
218252
*

src/Event/AuthenticateEvent.php

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
/**
5+
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
6+
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
7+
*
8+
* Licensed under The MIT License
9+
* For full copyright and license information, please see the LICENSE.txt
10+
* Redistributions of files must retain the above copyright notice.
11+
*
12+
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
13+
* @link https://cakephp.org CakePHP(tm) Project
14+
* @since 3.4.0
15+
* @license https://opensource.org/licenses/mit-license.php MIT License
16+
*/
17+
namespace Authentication\Event;
18+
19+
use Authentication\AuthenticationServiceInterface;
20+
use Authentication\Authenticator\AuthenticatorInterface;
21+
use Authentication\Authenticator\ResultInterface;
22+
use Cake\Event\Event;
23+
use InvalidArgumentException;
24+
use Psr\Http\Message\RequestInterface;
25+
use Psr\Http\Message\ServerRequestInterface;
26+
27+
/**
28+
* Event triggered when authentication is run.
29+
*
30+
* @extends \Cake\Event\Event<\Authentication\AuthenticationServiceInterface>
31+
*/
32+
class AuthenticateEvent extends Event
33+
{
34+
/**
35+
* Event name
36+
*
37+
* @var string
38+
*/
39+
public const NAME = 'Authentication.authenticate';
40+
41+
/**
42+
* Constructor
43+
*
44+
* @param string $name Name of the event
45+
* @param \Authentication\AuthenticationServiceInterface $subject The Authentication service instance this event applies to.
46+
* @param \Psr\Http\Message\ServerRequestInterface $request The request instance.
47+
* @param \Authentication\Authenticator\AuthenticatorInterface $authenticator The authenticator instance.
48+
* @param \Authentication\Authenticator\ResultInterface $result The authentication result.
49+
*/
50+
public function __construct(
51+
string $name,
52+
AuthenticationServiceInterface $subject,
53+
ServerRequestInterface $request,
54+
AuthenticatorInterface $authenticator,
55+
ResultInterface $result,
56+
) {
57+
$this->result = $result;
58+
59+
parent::__construct($name, $subject, compact('request', 'authenticator'));
60+
}
61+
62+
/**
63+
* The authentication result.
64+
*
65+
* @return \Authentication\Authenticator\ResultInterface
66+
*/
67+
public function getResult(): ResultInterface
68+
{
69+
return $this->result;
70+
}
71+
72+
/**
73+
* Set the authentication result.
74+
*
75+
* @param \Authentication\Authenticator\ResultInterface|null $value The result to set.
76+
* @return $this
77+
*/
78+
public function setResult(mixed $value = null)
79+
{
80+
if (!$value instanceof ResultInterface) {
81+
throw new InvalidArgumentException(
82+
'The result for Authentication.authenticate event must be a '
83+
. '`Authentication\Authenticator\ResultInterface` instance.',
84+
);
85+
}
86+
87+
return parent::setResult($value);
88+
}
89+
90+
/**
91+
* Get the request instance.
92+
*
93+
* @return \Psr\Http\Message\RequestInterface
94+
*/
95+
public function getRequest(): RequestInterface
96+
{
97+
return $this->_data['request'];
98+
}
99+
100+
/**
101+
* Get the adapter options.
102+
*
103+
* @return \Authentication\Authenticator\AuthenticatorInterface
104+
*/
105+
public function getAuthenticator(): AuthenticatorInterface
106+
{
107+
return $this->_data['authenticator'];
108+
}
109+
}

tests/TestCase/AuthenticationServiceTest.php

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,13 @@
2222
use Authentication\Authenticator\AuthenticatorInterface;
2323
use Authentication\Authenticator\FormAuthenticator;
2424
use Authentication\Authenticator\Result;
25+
use Authentication\Event\AuthenticateEvent;
2526
use Authentication\Identifier\IdentifierCollection;
2627
use Authentication\Identifier\PasswordIdentifier;
2728
use Authentication\Identity;
2829
use Authentication\IdentityInterface;
2930
use Authentication\Test\TestCase\AuthenticationTestCase as TestCase;
31+
use Cake\Event\EventManager;
3032
use Cake\Http\Exception\UnauthorizedException;
3133
use Cake\Http\Response;
3234
use Cake\Http\ServerRequest;
@@ -106,6 +108,69 @@ public function testAuthenticate()
106108
$this->assertInstanceOf(PasswordIdentifier::class, $identifier);
107109
}
108110

111+
public function testAuthenticateSuccessEvent(): void
112+
{
113+
$request = ServerRequestFactory::fromGlobals(
114+
['REQUEST_URI' => '/testpath'],
115+
[],
116+
['username' => 'mariano', 'password' => 'password'],
117+
);
118+
119+
$service = new AuthenticationService([
120+
'authenticators' => [
121+
'Authentication.Form' => [
122+
'identifier' => 'Authentication.Password',
123+
],
124+
],
125+
]);
126+
127+
EventManager::instance()->on(
128+
'Authentication.authenticate',
129+
function (AuthenticateEvent $event): void {
130+
$event->setResult(new Result(
131+
['user' => 'admad'],
132+
Result::SUCCESS,
133+
));
134+
},
135+
);
136+
137+
$result = $service->authenticate($request);
138+
$this->assertTrue($result->isValid());
139+
$this->assertSame(['user' => 'admad'], $result->getData());
140+
}
141+
142+
public function testAuthenticateFailureEvent(): void
143+
{
144+
$request = ServerRequestFactory::fromGlobals(
145+
['REQUEST_URI' => '/testpath'],
146+
[],
147+
['username' => 'mariano', 'password' => ''],
148+
);
149+
150+
$service = new AuthenticationService([
151+
'authenticators' => [
152+
'Authentication.Form' => [
153+
'identifier' => 'Authentication.Password',
154+
],
155+
],
156+
]);
157+
158+
EventManager::instance()->on(
159+
'Authentication.authenticate',
160+
function (AuthenticateEvent $event): void {
161+
$event->setResult(new Result(
162+
['user' => 'admad'],
163+
Result::FAILURE_OTHER,
164+
));
165+
},
166+
);
167+
168+
$result = $service->authenticate($request);
169+
$this->assertFalse($result->isValid());
170+
$this->assertSame(Result::FAILURE_OTHER, $result->getStatus());
171+
$this->assertSame(['user' => 'admad'], $result->getData());
172+
}
173+
109174
/**
110175
* test authenticate() with a challenger authenticator
111176
*

0 commit comments

Comments
 (0)