Skip to content

Commit e498c11

Browse files
authored
Merge pull request #28 from activecollab/authentication-intent
Authentication intent
2 parents 7a2bc24 + 29ffb46 commit e498c11

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+738
-243
lines changed

src/Adapter/BrowserSessionAdapter.php

+10-6
Original file line numberDiff line numberDiff line change
@@ -56,15 +56,19 @@ public function initialize(ServerRequestInterface $request): ?TransportInterface
5656

5757
$session = $this->session_repository->getById($session_id);
5858

59-
if ($session instanceof SessionInterface) {
60-
if ($user = $session->getAuthenticatedUser($this->user_repository)) {
61-
$this->session_repository->recordUsageBySession($session);
59+
if (!$session) {
60+
return new CleanUpTransport($this);
61+
}
6262

63-
return new AuthenticationTransport($this, $user, $session);
64-
}
63+
$user = $session->getAuthenticatedUser($this->user_repository);
64+
65+
if (!$user) {
66+
return new CleanUpTransport($this);
6567
}
6668

67-
return new CleanUpTransport($this);
69+
$this->session_repository->recordUsageBySession($session);
70+
71+
return new AuthenticationTransport($this, $user, $session);
6872
}
6973

7074
public function applyToResponse(ResponseInterface $response, TransportInterface $transport): ResponseInterface

src/AuthenticatedUser/AuthenticatedUserInterface.php

+7-1
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,15 @@
1010

1111
namespace ActiveCollab\Authentication\AuthenticatedUser;
1212

13+
use ActiveCollab\Authentication\AuthenticatedUser\Username\UsernameInterface;
1314
use ActiveCollab\User\UserInterface;
1415

1516
interface AuthenticatedUserInterface extends UserInterface
1617
{
1718
/**
1819
* Return username that is used for authentication (email, unique username, phone number...).
1920
*/
20-
public function getUsername(): string;
21+
public function getUsername(): UsernameInterface;
2122

2223
/**
2324
* Check if $password is a valid password of this user.
@@ -28,4 +29,9 @@ public function isValidPassword(string $password): bool;
2829
* Return true if this user can authenticate.
2930
*/
3031
public function canAuthenticate(): bool;
32+
33+
/**
34+
* Return true if this user requires second factor to authenticate.
35+
*/
36+
public function requiresSecondFactor(): bool;
3137
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Active Collab Authentication project.
5+
*
6+
* (c) A51 doo <[email protected]>. All rights reserved.
7+
*/
8+
9+
declare(strict_types=1);
10+
11+
namespace ActiveCollab\Authentication\AuthenticatedUser\Username;
12+
13+
use Stringable;
14+
15+
interface UsernameInterface extends Stringable
16+
{
17+
public function getUsername(): string;
18+
}

src/Authentication.php

+124-8
Original file line numberDiff line numberDiff line change
@@ -15,28 +15,45 @@
1515
use ActiveCollab\Authentication\AuthenticationResult\AuthenticationResultInterface;
1616
use ActiveCollab\Authentication\AuthenticationResult\Transport\Authentication\AuthenticationTransportInterface;
1717
use ActiveCollab\Authentication\AuthenticationResult\Transport\Authorization\AuthorizationTransport;
18+
use ActiveCollab\Authentication\AuthenticationResult\Transport\Authorization\AuthorizationTransportInterface;
1819
use ActiveCollab\Authentication\AuthenticationResult\Transport\Transport;
1920
use ActiveCollab\Authentication\AuthenticationResult\Transport\TransportInterface;
2021
use ActiveCollab\Authentication\Authorizer\AuthorizerInterface;
22+
use ActiveCollab\Authentication\Exception\IntentExpiredException;
23+
use ActiveCollab\Authentication\Exception\IntentFulfilledException;
2124
use ActiveCollab\Authentication\Exception\InvalidAuthenticationRequestException;
25+
use ActiveCollab\Authentication\Exception\InvalidIntentUserException;
26+
use ActiveCollab\Authentication\Exception\SecondFactorNotRequiredException;
27+
use ActiveCollab\Authentication\Intent\IntentInterface;
28+
use ActiveCollab\Authentication\Intent\RepositoryInterface as IntentRepositoryInterface;
2229
use Exception;
30+
use LogicException;
2331
use Psr\Http\Message\ResponseInterface;
2432
use Psr\Http\Message\ServerRequestInterface;
2533
use Psr\Http\Server\RequestHandlerInterface;
2634

2735
class Authentication implements AuthenticationInterface
2836
{
37+
private IntentRepositoryInterface $intent_repository;
2938
private array $adapters;
3039
private ?AuthenticatedUserInterface $authenticated_user = null;
3140
private ?AuthenticationResultInterface $authenticated_with = null;
3241
private array $on_user_authenticated = [];
3342
private array $on_user_authorized = [];
3443
private array $on_user_authorization_failed = [];
44+
private array $on_intent_authorized = [];
45+
private array $on_intent_authorization_failed = [];
46+
private array $on_intent_fulfilled = [];
47+
private array $on_intent_fulfillment_failed = [];
3548
private array $on_user_set = [];
3649
private array $on_user_deauthenticated = [];
3750

38-
public function __construct(AdapterInterface ...$adapters)
51+
public function __construct(
52+
IntentRepositoryInterface $intent_repository,
53+
AdapterInterface ...$adapters,
54+
)
3955
{
56+
$this->intent_repository = $intent_repository;
4057
$this->adapters = $adapters;
4158
}
4259

@@ -114,26 +131,93 @@ public function authorize(
114131
AuthorizerInterface $authorizer,
115132
AdapterInterface $adapter,
116133
array $credentials,
117-
$payload = null
118-
): TransportInterface
134+
array $payload = null,
135+
): TransportInterface|IntentInterface
119136
{
120137
try {
121138
$user = $authorizer->verifyCredentials($credentials);
122-
$authenticatedWith = $adapter->authenticate($user, $credentials);
123139

124-
$this->triggerEvent('user_authorized', $user, $authenticatedWith);
140+
if ($user->requiresSecondFactor()) {
141+
$intent = $this->intent_repository->createSecondFactorIntent(
142+
$user,
143+
);
144+
145+
$this->triggerEvent('intent_authorized', $user, $intent);
125146

126-
$authorizationResult = new AuthorizationTransport($adapter, $user, $authenticatedWith, $payload);
127-
$this->lastProcessingResult = $authorizationResult;
147+
return $intent;
148+
}
128149

129-
return $authorizationResult;
150+
return $this->completeAuthentication(
151+
$adapter,
152+
$user,
153+
$credentials,
154+
$payload,
155+
);
130156
} catch (Exception $e) {
131157
$this->triggerEvent('user_authorization_failed', $credentials, $e);
132158

133159
throw $e;
134160
}
135161
}
136162

163+
public function fulfillIntent(
164+
AuthorizerInterface $authorizer,
165+
AdapterInterface $adapter,
166+
IntentInterface $intent,
167+
AuthenticatedUserInterface $user,
168+
array $credentials,
169+
array $payload = null,
170+
): TransportInterface
171+
{
172+
try {
173+
if ($intent->isFulfilled()) {
174+
throw new IntentFulfilledException();
175+
}
176+
177+
if ($intent->isExpired()) {
178+
throw new IntentExpiredException();
179+
}
180+
181+
if (!$intent->belongsToUser($user)) {
182+
throw new InvalidIntentUserException();
183+
}
184+
185+
if (!$user->requiresSecondFactor()) {
186+
throw new SecondFactorNotRequiredException();
187+
}
188+
189+
$intent->fulfill($user, $credentials);
190+
$this->triggerEvent('intent_fulfilled', $user, $intent);
191+
192+
return $this->completeAuthentication(
193+
$adapter,
194+
$user,
195+
$credentials,
196+
$payload,
197+
);
198+
} catch (Exception $e) {
199+
$this->triggerEvent('intent_fulfillment_failed', $credentials, $e);
200+
throw $e;
201+
}
202+
}
203+
204+
private function completeAuthentication(
205+
AdapterInterface $adapter,
206+
AuthenticatedUserInterface $user,
207+
array $credentials,
208+
?array $payload,
209+
): AuthorizationTransportInterface
210+
{
211+
$authenticatedWith = $adapter->authenticate($user, $credentials);
212+
213+
$this->triggerEvent('user_authorized', $user, $authenticatedWith);
214+
215+
$authorizationResult = new AuthorizationTransport($adapter, $user, $authenticatedWith, $payload);
216+
$this->lastProcessingResult = $authorizationResult;
217+
218+
return $authorizationResult;
219+
}
220+
137221
public function terminate(
138222
AdapterInterface $adapter,
139223
AuthenticationResultInterface $authenticatedWith
@@ -217,6 +301,10 @@ private function triggerEvent(string $event_name, ...$arguments): void
217301
{
218302
$property_name = sprintf("on_%s", $event_name);
219303

304+
if (!property_exists($this, $property_name)) {
305+
throw new LogicException(sprintf('Event %s does not exist.', $event_name));
306+
}
307+
220308
/** @var callable $handler */
221309
foreach ($this->$property_name as $handler) {
222310
call_user_func_array($handler, $arguments);
@@ -244,6 +332,34 @@ public function onUserAuthorizationFailed(callable $value): AuthenticationInterf
244332
return $this;
245333
}
246334

335+
public function onIntentAuthorized(callable $value): AuthenticationInterface
336+
{
337+
$this->on_intent_authorized[] = $value;
338+
339+
return $this;
340+
}
341+
342+
public function onIntentAuthorizationFailed(callable $value): AuthenticationInterface
343+
{
344+
$this->on_intent_authorization_failed[] = $value;
345+
346+
return $this;
347+
}
348+
349+
public function onIntentFulfilled(callable $value): AuthenticationInterface
350+
{
351+
$this->on_intent_fulfilled[] = $value;
352+
353+
return $this;
354+
}
355+
356+
public function onIntentFulfillmentFailed(callable $value): AuthenticationInterface
357+
{
358+
$this->on_intent_fulfillment_failed[] = $value;
359+
360+
return $this;
361+
}
362+
247363
public function onUserSet(callable $value): AuthenticationInterface
248364
{
249365
$this->on_user_set[] = $value;

src/AuthenticationInterface.php

+17-3
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
use ActiveCollab\Authentication\AuthenticationResult\AuthenticationResultInterface;
1616
use ActiveCollab\Authentication\AuthenticationResult\Transport\TransportInterface;
1717
use ActiveCollab\Authentication\Authorizer\AuthorizerInterface;
18+
use ActiveCollab\Authentication\Intent\IntentInterface;
1819
use Psr\Http\Message\ResponseInterface;
1920
use Psr\Http\Message\ServerRequestInterface;
2021
use Psr\Http\Server\MiddlewareInterface;
@@ -24,19 +25,28 @@ interface AuthenticationInterface extends MiddlewareInterface
2425
public function __invoke(
2526
ServerRequestInterface $request,
2627
ResponseInterface $response,
27-
callable $next = null
28+
callable $next = null,
2829
): ResponseInterface;
2930

3031
public function authorize(
3132
AuthorizerInterface $authorizer,
3233
AdapterInterface $adapter,
3334
array $credentials,
34-
$payload = null
35+
array $payload = null,
36+
): TransportInterface|IntentInterface;
37+
38+
public function fulfillIntent(
39+
AuthorizerInterface $authorizer,
40+
AdapterInterface $adapter,
41+
IntentInterface $intent,
42+
AuthenticatedUserInterface $user,
43+
array $credentials,
44+
array $payload,
3545
): TransportInterface;
3646

3747
public function terminate(
3848
AdapterInterface $adapter,
39-
AuthenticationResultInterface $authenticatedWith
49+
AuthenticationResultInterface $authenticatedWith,
4050
): TransportInterface;
4151

4252
/**
@@ -53,6 +63,10 @@ public function setAuthenticatedWith(?AuthenticationResultInterface $value): Aut
5363
public function onUserAuthenticated(callable $value): AuthenticationInterface;
5464
public function onUserAuthorized(callable $value): AuthenticationInterface;
5565
public function onUserAuthorizationFailed(callable $value): AuthenticationInterface;
66+
public function onIntentAuthorized(callable $value): AuthenticationInterface;
67+
public function onIntentAuthorizationFailed(callable $value): AuthenticationInterface;
68+
public function onIntentFulfilled(callable $value): AuthenticationInterface;
69+
public function onIntentFulfillmentFailed(callable $value): AuthenticationInterface;
5670
public function onUserSet(callable $value): AuthenticationInterface;
5771
public function onUserDeauthenticated(callable $value): AuthenticationInterface;
5872
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace ActiveCollab\Authentication\Authorizer\SecondFactor;
6+
7+
use ActiveCollab\Authentication\AuthenticatedUser\AuthenticatedUserInterface;
8+
9+
interface SecondFactorVerifierInterface
10+
{
11+
public function verifySecondFactorCode(
12+
string $secondFactorCode,
13+
AuthenticatedUserInterface $user,
14+
): bool;
15+
}

src/Exception/AuthenticationDisabledException.php

+2-8
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,10 @@
1010

1111
namespace ActiveCollab\Authentication\Exception;
1212

13-
use Exception as PhpException;
14-
1513
class AuthenticationDisabledException extends RuntimeException
1614
{
17-
public function __construct(
18-
string $message = 'Authentication is temporary disabled',
19-
int $code = 0,
20-
PhpException $previous = null,
21-
)
15+
protected function getAuthExceptionMessage(): string
2216
{
23-
parent::__construct($message, $code, $previous);
17+
return 'Authentication is temporary disabled.';
2418
}
2519
}
+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Active Collab Authentication project.
5+
*
6+
* (c) A51 doo <[email protected]>. All rights reserved.
7+
*/
8+
9+
declare(strict_types=1);
10+
11+
namespace ActiveCollab\Authentication\Exception;
12+
13+
class IntentExpiredException extends RuntimeException
14+
{
15+
protected function getAuthExceptionMessage(): string
16+
{
17+
return 'Intent expired.';
18+
}
19+
}
+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Active Collab Authentication project.
5+
*
6+
* (c) A51 doo <[email protected]>. All rights reserved.
7+
*/
8+
9+
declare(strict_types=1);
10+
11+
namespace ActiveCollab\Authentication\Exception;
12+
13+
class IntentFulfilledException extends RuntimeException
14+
{
15+
protected function getAuthExceptionMessage(): string
16+
{
17+
return 'Intent already fulfilled.';
18+
}
19+
}

0 commit comments

Comments
 (0)