Skip to content

Commit 5e38d0d

Browse files
authored
Merge pull request #13 from activecollab/feature/auth-events
Add authentication events.
2 parents b02a4a4 + 3ec7057 commit 5e38d0d

6 files changed

+447
-32
lines changed

README.md

+35-2
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@ Table of Contents:
88
* [Accessing Users](#accessing-users)
99
* [Authorizers](#authorizers)
1010
* [Request Aware Auhtorizers](#request-aware-auhtorizers)
11+
* [Exception Aware Authorizers](#exception-aware-auhtorizers)
1112
* [Transports](#transports)
13+
* [Events](#events)
1214
* [Authentication Middlewares](#authentication-middlewares)
1315
* [Working with Passwords](#working-with-passwords)
1416
* [Hashing and Validating Passwords](#hashing-and-validating-passwords)
@@ -255,6 +257,37 @@ if (!$transport->isApplied()) {
255257
}
256258
```
257259

260+
## Events
261+
262+
Authentication utility throws events for which you can write handlers. Here's an example:
263+
264+
```php
265+
<?php
266+
267+
namespace MyApp;
268+
269+
use ActiveCollab\Authentication\Authentication;
270+
271+
$auth = new Authentication([]);
272+
$auth->onUserAuthorizationFailed(function(array $credentials) {
273+
// Log attempt for user's username.
274+
});
275+
$auth->onUserAuthorizationFailed(function(array $credentials) {
276+
// If third attempt, notify administrator that particular user has trouble logging in.
277+
});
278+
$auth->onUserAuthorizationFailed(function(array $credentials) {
279+
// If fifth attempt, block IP address for a couple of minutes, to cool it down.
280+
});
281+
```
282+
283+
As you can see from the example above, you can provide multiple handlers for the same event. Following events are available:
284+
285+
1. `onUserAuthenticated` - (visit) User is recognized by its session cookie, token etc, so it was authenticated. Arguments provided to the callaback are user instance [AuthenticatedUserInterface], and authentication result [AuthenticationResultInterface].
286+
1. `onUserAuthorized` (login) User provided valid credentials, and system authorized it. Arguments provided to the callaback are user instance [AuthenticatedUserInterface], and authentication result [AuthenticationResultInterface].
287+
1. `onUserAuthorizationFailed` (login failed) User tried to authorize, but provided credentials were not valid, or authorization failed due to other reasons (SSO service down, etc). Arguments provided to the callback are user's credentials [array], as well as the failure reason ([Exception] or [Throwable]).
288+
1. `onUserSet` - User is set - authenticated, authorizer, or app set the user using its own logic. Argument provided to the callback is the user instance [AuthenticatedUserInterface].
289+
1. `setOnUserDeauthenticated` (logout) User logged out. Argument provided to the callback is authentication method that got terminated [AuthenticationResultInterface].
290+
258291
## Authentication Middlewares
259292

260293
`AuthenticationInterface` interface assumes that implementation will be such that it can be invoked as a middleware in a [PSR-7](http://www.php-fig.org/psr/psr-7/) middleware stack. That is why implementation of `__invoke` method in middleware stack fashion is part of the interface.
@@ -446,5 +479,5 @@ Login policy implements `\JsonSerializable` interface, and can be safely encoded
446479

447480
## To Do
448481

449-
1. Consider adding previously used passwords repository, so library can enforce no-repeat policy for passwords,
450-
1. Add `onAuthentication()`, `onAuthorization()`, `onDeauthentication()` events to `AuthenticationInterface`.
482+
1. Consider adding previously used passwords repository, so library can enforce no-repeat policy for passwords.
483+
1. Remove deprecated `AuthenticationInterface::setOnAuthenciatedUserChanged()`.

src/Authentication.php

+108-11
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
use ActiveCollab\Authentication\Authorizer\AuthorizerInterface;
1919
use ActiveCollab\Authentication\Exception\InvalidAuthenticationRequestException;
2020
use Exception;
21+
use InvalidArgumentException;
2122
use LogicException;
2223
use Psr\Http\Message\ResponseInterface;
2324
use Psr\Http\Message\ServerRequestInterface;
@@ -45,9 +46,29 @@ class Authentication implements AuthenticationInterface
4546
private $authenticated_with;
4647

4748
/**
48-
* @var callable|null
49+
* @var callable[]
4950
*/
50-
private $on_authencated_user_changed;
51+
private $on_user_authenticated = [];
52+
53+
/**
54+
* @var callable[]
55+
*/
56+
private $on_user_authorized = [];
57+
58+
/**
59+
* @var callable[]
60+
*/
61+
private $on_user_authorization_failed = [];
62+
63+
/**
64+
* @var callable[]
65+
*/
66+
private $on_user_set = [];
67+
68+
/**
69+
* @var callable[]
70+
*/
71+
private $on_user_deauthenticated = [];
5172

5273
/**
5374
* @param array $adapters
@@ -74,6 +95,8 @@ public function __invoke(ServerRequestInterface $request, ResponseInterface $res
7495
if ($auth_result instanceof AuthenticationTransportInterface) {
7596
$this->setAuthenticatedUser($auth_result->getAuthenticatedUser());
7697
$this->setAuthenticatedWith($auth_result->getAuthenticatedWith());
98+
99+
$this->triggerEvent('user_authenticated', $auth_result->getAuthenticatedUser(), $auth_result->getAuthenticatedWith());
77100
}
78101

79102
list($request, $response) = $auth_result->applyTo($request, $response);
@@ -91,18 +114,30 @@ public function __invoke(ServerRequestInterface $request, ResponseInterface $res
91114
*/
92115
public function authorize(AuthorizerInterface $authorizer, AdapterInterface $adapter, array $credentials, $payload = null)
93116
{
94-
$user = $authorizer->verifyCredentials($credentials);
95-
$authenticated_with = $adapter->authenticate($user, $credentials);
117+
try {
118+
$user = $authorizer->verifyCredentials($credentials);
119+
$authenticated_with = $adapter->authenticate($user, $credentials);
120+
121+
$this->triggerEvent('user_authorized', $user, $authenticated_with);
96122

97-
return new AuthorizationTransport($adapter, $user, $authenticated_with, $payload);
123+
return new AuthorizationTransport($adapter, $user, $authenticated_with, $payload);
124+
} catch (Exception $e) {
125+
$this->triggerEvent('user_authorization_failed', $credentials, $e);
126+
127+
throw $e;
128+
}
98129
}
99130

100131
/**
101132
* {@inheritdoc}
102133
*/
103134
public function terminate(AdapterInterface $adapter, AuthenticationResultInterface $authenticated_with)
104135
{
105-
return $adapter->terminate($authenticated_with);
136+
$termination_result = $adapter->terminate($authenticated_with);
137+
138+
$this->triggerEvent('user_deauthenticated', $authenticated_with);
139+
140+
return $termination_result;
106141
}
107142

108143
/**
@@ -166,9 +201,7 @@ public function &setAuthenticatedUser(AuthenticatedUserInterface $user = null)
166201
{
167202
$this->authenticated_user = $user;
168203

169-
if (is_callable($this->on_authencated_user_changed)) {
170-
call_user_func($this->on_authencated_user_changed, $user);
171-
}
204+
$this->triggerEvent('user_set', $user);
172205

173206
return $this;
174207
}
@@ -191,13 +224,77 @@ public function &setAuthenticatedWith(AuthenticationResultInterface $value)
191224
return $this;
192225
}
193226

227+
/**
228+
* Trigger an internal event.
229+
*
230+
* @param string $event_name
231+
* @param array $arguments
232+
* @return $this
233+
*/
234+
private function &triggerEvent($event_name, ...$arguments)
235+
{
236+
$property_name = "on_{$event_name}";
237+
238+
/** @var callable $handler */
239+
foreach ($this->$property_name as $handler) {
240+
call_user_func_array($handler, $arguments);
241+
}
242+
243+
return $this;
244+
}
245+
194246
/**
195247
* {@inheritdoc}
196248
*/
197-
public function &setOnAuthenciatedUserChanged(callable $value = null)
249+
public function &onUserAuthenticated(callable $value)
198250
{
199-
$this->on_authencated_user_changed = $value;
251+
$this->on_user_authenticated[] = $value;
200252

201253
return $this;
202254
}
255+
256+
public function &onUserAuthorized(callable $value)
257+
{
258+
$this->on_user_authorized[] = $value;
259+
260+
return $this;
261+
}
262+
263+
public function &onUserAuthorizationFailed(callable $value)
264+
{
265+
$this->on_user_authorization_failed[] = $value;
266+
267+
return $this;
268+
}
269+
270+
/**
271+
* {@inheritdoc}
272+
*/
273+
public function &onUserSet(callable $value)
274+
{
275+
$this->on_user_set[] = $value;
276+
277+
return $this;
278+
}
279+
280+
public function &setOnUserDeauthenticated(callable $value)
281+
{
282+
$this->on_user_deauthenticated[] = $value;
283+
284+
return $this;
285+
}
286+
287+
/**
288+
* Kept for backward compatibility reasons. Will be removed.
289+
*
290+
* {@inheritdoc}
291+
*/
292+
public function &setOnAuthenciatedUserChanged(callable $value = null)
293+
{
294+
if (empty($value)) {
295+
throw new InvalidArgumentException('Value needs to be a callable.');
296+
}
297+
298+
return $this->onUserSet($value);
299+
}
203300
}

src/AuthenticationInterface.php

+33
Original file line numberDiff line numberDiff line change
@@ -83,8 +83,41 @@ public function getAuthenticatedWith();
8383
public function &setAuthenticatedWith(AuthenticationResultInterface $value);
8484

8585
/**
86+
* @param callable $value
87+
* @return $this
88+
*/
89+
public function &onUserAuthenticated(callable $value);
90+
91+
/**
92+
* @param callable $value
93+
* @return $this
94+
*/
95+
public function &onUserAuthorized(callable $value);
96+
97+
/**
98+
* @param callable $value
99+
* @return $this
100+
*/
101+
public function &onUserAuthorizationFailed(callable $value);
102+
103+
/**
104+
* @param callable $value
105+
* @return $this
106+
*/
107+
public function &onUserSet(callable $value);
108+
109+
/**
110+
* @param callable $value
111+
* @return $this
112+
*/
113+
public function &setOnUserDeauthenticated(callable $value);
114+
115+
/**
116+
* Use onUserSet() instead.
117+
*
86118
* @param callable|null $value
87119
* @return $this
120+
* @deprecated
88121
*/
89122
public function &setOnAuthenciatedUserChanged(callable $value = null);
90123
}

test/src/AuthenticationMiddlewareTest.php

-17
Original file line numberDiff line numberDiff line change
@@ -206,21 +206,4 @@ public function testExceptionOnMultipleIds()
206206

207207
call_user_func(new Authentication([$this->browser_session_adapter, $this->token_bearer_adapter]), $request, $response);
208208
}
209-
210-
public function testOnAuthenticatedUserCallback()
211-
{
212-
/** @var ServerRequestInterface $request */
213-
$request = $this->request->withHeader('Authorization', 'Bearer awesome-token');
214-
215-
$middleware = new Authentication([$this->token_bearer_adapter]);
216-
217-
$callback_is_called = false;
218-
$middleware->setOnAuthenciatedUserChanged(function () use (&$callback_is_called) {
219-
$callback_is_called = true;
220-
});
221-
222-
call_user_func($middleware, $request, $this->response);
223-
224-
$this->assertTrue($callback_is_called);
225-
}
226209
}

0 commit comments

Comments
 (0)