AuthenticatorManager::checkPassportCredentials()   A
last analyzed

Complexity

Conditions 6
Paths 5

Size

Total Lines 30
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 16
c 0
b 0
f 0
dl 0
loc 30
rs 9.1111
cc 6
nc 5
nop 1
1
<?php
2
3
/**
4
 * This file is part of web-stack
5
 *
6
 * For the full copyright and license information, please view the LICENSE
7
 * file that was distributed with this source code.
8
 */
9
10
declare(strict_types=1);
11
12
namespace Slick\WebStack\Domain\Security\Http;
13
14
use Slick\WebStack\Domain\Security\Authentication\Token\TokenStorageInterface;
15
use Slick\WebStack\Domain\Security\Exception\AuthenticationException;
16
use Slick\WebStack\Domain\Security\Exception\BadCredentialsException;
17
use Slick\WebStack\Domain\Security\Http\Authenticator\Passport\Badge\Credentials\PasswordCredentials;
18
use Slick\WebStack\Domain\Security\PasswordHasher\PasswordHasherInterface;
19
use Slick\WebStack\Domain\Security\SecurityException;
20
use Slick\WebStack\Domain\Security\User\PasswordAuthenticatedUserInterface;
21
use Slick\WebStack\Domain\Security\User\PasswordUpgradableInterface;
22
use Slick\WebStack\Domain\Security\UserInterface;
23
use InvalidArgumentException;
24
use Psr\Http\Message\ResponseInterface;
25
use Psr\Http\Message\ServerRequestInterface;
26
use Psr\Log\LoggerInterface;
27
28
/**
29
 * AuthenticatorManager
30
 *
31
 * @package Slick\WebStack\Domain\Security\Http
32
 */
33
final class AuthenticatorManager implements AuthenticatorManagerInterface
34
{
35
    public const AUTHENTICATORS_ATTRIBUTE_KEY = '_security_authenticators';
36
    public const SKIPPED_AUTHENTICATORS_ATTRIBUTE_KEY = '_security_skipped_authenticators';
37
38
    /** @var array<string> */
39
    private array $errors = [];
40
41
    /**
42
     * Creates a AuthenticatorManager
43
     *
44
     * @template T of UserInterface
45
     * @param iterable<AuthenticatorInterface<T>> $authenticators
46
     * @param TokenStorageInterface<T> $tokenStorage
47
     * @param PasswordHasherInterface $hasher
48
     * @param LoggerInterface|null $logger
49
     */
50
    public function __construct(
51
        private readonly iterable $authenticators,
52
        private readonly TokenStorageInterface $tokenStorage,
53
        private readonly PasswordHasherInterface $hasher,
54
        private readonly ?LoggerInterface $logger = null
55
    ) {
56
    }
57
58
    /**
59
     * @inheritDoc
60
     * @throws InvalidArgumentException If at least one authenticator doesn't implement AuthenticatorInterface
61
     */
62
    public function supports(ServerRequestInterface &$request): bool
63
    {
64
        $authenticators = [];
65
        $skipped = [];
66
        foreach ($this->authenticators as $authenticator) {
67
            $this->logger?->debug('Checking support on authenticator.', ['authenticator' => $authenticator::class]);
68
            $this->checkAuthenticator($authenticator);
69
70
            if ($authenticator->supports($request)) {
71
                $authenticators[] = $authenticator;
72
                continue;
73
            }
74
75
            $skipped[] = $authenticator;
76
            $this->logger?->debug(
77
                'Authenticator does not support the request.',
78
                ['authenticator' => $authenticator::class]
79
            );
80
        }
81
82
        $request = $request->withAttribute(self::AUTHENTICATORS_ATTRIBUTE_KEY, $authenticators);
83
        $request = $request->withAttribute(self::SKIPPED_AUTHENTICATORS_ATTRIBUTE_KEY, $skipped);
84
85
        return !empty($authenticators);
86
    }
87
88
    /**
89
     * @inheritDoc
90
     *
91
     * @throws SecurityException
92
     */
93
    public function authenticateRequest(ServerRequestInterface $request): ?ResponseInterface
94
    {
95
        $authenticators = $request->getAttribute(self::AUTHENTICATORS_ATTRIBUTE_KEY, []);
96
        foreach ($authenticators as $authenticator) {
97
            $response = $this->executeAuthenticator($authenticator, $request);
98
            if (null !== $response) {
99
                return $response;
100
            }
101
        }
102
103
        return null;
104
    }
105
106
    /**
107
     * Checks if the provided authenticator implements the AuthenticatorInterface.
108
     *
109
     * @param mixed $authenticator The authenticator to check.
110
     *
111
     * @throws InvalidArgumentException If the provided authenticator does not implement the AuthenticatorInterface.
112
     */
113
    public function checkAuthenticator(mixed $authenticator): void
114
    {
115
        if ($authenticator instanceof AuthenticatorInterface) {
116
            return;
117
        }
118
119
        throw new InvalidArgumentException(
120
            sprintf(
121
                'Authenticator "%s" must implement "%s".',
122
                get_debug_type($authenticator),
123
                AuthenticatorInterface::class
124
            )
125
        );
126
    }
127
128
    /**
129
     * Executes the specified Authenticator with the given request.
130
     *
131
     * @param AuthenticatorInterface $authenticator The authenticator to execute.
132
     * @param ServerRequestInterface $request The request to authenticate.
133
     * @return ResponseInterface|null The response from the authenticator, or null if no response is returned.
134
     * @throws SecurityException
135
     *
136
     * @template T of UserInterface
137
     * @phpstan-param AuthenticatorInterface<T> $authenticator
138
     */
139
    private function executeAuthenticator(
140
        AuthenticatorInterface $authenticator,
141
        ServerRequestInterface $request
142
    ): ?ResponseInterface {
143
144
        try {
145
            // get the passport from the Authenticator
146
            $passport = $authenticator->authenticate($request);
147
148
            // check the passport (e.g. password checking)
149
            $this->checkPassportCredentials($passport);
150
151
            // check if all badges are resolved
152
            foreach ($passport->badges() as $badge) {
153
                if (!$badge->isResolved()) {
154
                    $lastNamespaceParts = explode('\\', get_debug_type($badge));
155
                    throw new BadCredentialsException(
156
                        sprintf(
157
                            'Authentication failed: Security badge "%s" is not resolved.',
158
                            end($lastNamespaceParts)
159
                        )
160
                    );
161
                }
162
            }
163
            // create the authentication token
164
            $authenticationToken = $authenticator->createToken($passport);
165
166
            $this->logger?->info(
167
                'Authenticator successful!',
168
                ['token' => $authenticationToken, 'authenticator' => $authenticator::class]
169
            );
170
        } catch (AuthenticationException $exception) {
171
            $response = $authenticator->onAuthenticationFailure($request, $exception);
172
            if ($exception->getCode() == 0) {
173
                $this->errors[$exception::class] = $exception->getMessage();
174
            }
175
            return  ($response instanceof ResponseInterface) ? $response : null;
176
        }
177
178
        // success! (sets the token on the token storage, etc)
179
        $this->tokenStorage->setToken($authenticationToken);
180
        $response = $authenticator->onAuthenticationSuccess($request, $authenticationToken);
181
        if ($response instanceof ResponseInterface) {
182
            return $response;
183
        }
184
185
        $this->logger?->debug(
186
            'Authenticator set no success response: request continues.',
187
            ['authenticator' => $authenticator::class]
188
        );
189
190
        return null;
191
    }
192
193
    /**
194
     * Checks the credentials in the passport.
195
     *
196
     * @template T of UserInterface
197
     * @param Authenticator\PassportInterface<T> $passport The authentication passport.
198
     *
199
     * @throws AuthenticationException When the user object does not implement PasswordAuthenticatedUserInterface.
200
     * @throws BadCredentialsException When the password couldn't be verified.
201
     * @throws SecurityException
202
     */
203
    private function checkPassportCredentials(Authenticator\PassportInterface $passport): void
204
    {
205
        if (!$passport->hasBadge(PasswordCredentials::class)) {
206
            return;
207
        }
208
209
        $user = $passport->user();
210
        if (!$user instanceof PasswordAuthenticatedUserInterface) {
211
            throw new AuthenticationException(
212
                sprintf(
213
                    'User object "%s" must implement "%s".',
214
                    get_debug_type($user),
215
                    PasswordAuthenticatedUserInterface::class
216
                )
217
            );
218
        }
219
220
        $hashedPassword = $user->password();
221
        /** @var PasswordCredentials $credentials */
222
        $credentials = $passport->badge(PasswordCredentials::class);
223
224
225
        if (!$this->hasher->verify($hashedPassword, $credentials->password())) {
226
            throw new BadCredentialsException("Password couldn't be verified.");
227
        }
228
229
        if ($this->hasher->needsRehash($hashedPassword) && $user instanceof PasswordUpgradableInterface) {
230
            $user->upgradePassword($this->hasher->hash($credentials->password()));
231
        }
232
        $credentials->markResolved();
233
    }
234
235
    /**
236
     * @inheritDoc
237
     */
238
    
239
    public function authenticationErrors(): array
240
    {
241
        return $this->errors;
242
    }
243
244
    /**
245
     * @inheritDoc
246
     */
247
    public function clear(): void
248
    {
249
        foreach ($this->authenticators as $authenticator) {
250
            $authenticator->clear();
251
        }
252
    }
253
}
254