Passed
Push — master ( 279881...b4f44a )
by Divine Niiquaye
10:48
created

Authenticator::has()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 1
nc 1
nop 1
dl 0
loc 3
ccs 0
cts 2
cp 0
crap 2
rs 10
c 1
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * This file is part of Biurad opensource projects.
7
 *
8
 * PHP version 7.4 and above required
9
 *
10
 * @author    Divine Niiquaye Ibok <[email protected]>
11
 * @copyright 2019 Biurad Group (https://biurad.com/)
12
 * @license   https://opensource.org/licenses/BSD-3-Clause License
13
 *
14
 * For the full copyright and license information, please view the LICENSE
15
 * file that was distributed with this source code.
16
 *
17
 */
18
19
namespace Biurad\Security;
20
21
use Biurad\Security\Event\AuthenticationFailureEvent;
22
use Biurad\Security\Interfaces\AuthenticatorInterface;
23
use Biurad\Security\Interfaces\FailureHandlerInterface;
24
use Biurad\Security\Interfaces\RequireTokenInterface;
25
use Biurad\Security\RateLimiter\AbstractRequestRateLimiter;
26
use Psr\EventDispatcher\EventDispatcherInterface;
27
use Psr\Http\Message\ResponseInterface;
28
use Psr\Http\Message\ServerRequestInterface;
29
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
30
use Symfony\Component\Security\Core\Authentication\Token\NullToken;
31
use Symfony\Component\Security\Core\Authentication\Token\PreAuthenticatedToken;
32
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
33
use Symfony\Component\Security\Core\Authentication\Token\SwitchUserToken;
34
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
35
use Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface;
36
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
37
use Symfony\Component\Security\Core\Event\AuthenticationSuccessEvent;
38
use Symfony\Component\Security\Core\Exception\AccountStatusException;
39
use Symfony\Component\Security\Core\Exception\AuthenticationCredentialsNotFoundException;
40
use Symfony\Component\Security\Core\Exception\AuthenticationException;
41
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
42
use Symfony\Component\Security\Core\Exception\CustomUserMessageAccountStatusException;
43
use Symfony\Component\Security\Core\Exception\ProviderNotFoundException;
44
use Symfony\Component\Security\Core\Exception\TooManyLoginAttemptsAuthenticationException;
45
use Symfony\Component\Security\Core\Exception\UserNotFoundException;
46
use Symfony\Component\Security\Core\User\InMemoryUserChecker;
47
use Symfony\Component\Security\Core\User\UserCheckerInterface;
48
use Symfony\Component\Security\Core\User\UserInterface;
49
50
/**
51
 * Authenticate a user with a set of authenticators.
52
 *
53
 * @author Divine Niiquaye Ibok <[email protected]>
54
 */
55
class Authenticator implements AuthorizationCheckerInterface
56
{
57
    private TokenStorageInterface $tokenStorage;
58
    private AccessDecisionManagerInterface $accessDecisionManager;
59
    private UserCheckerInterface $userChecker;
60
    private ?PropertyAccessorInterface $propertyAccessor;
61
    private ?AbstractRequestRateLimiter $limiter;
62
    private ?EventDispatcherInterface $eventDispatcher;
63
    private bool $hideUserNotFoundExceptions, $eraseCredentials;
64
    private string $firewallName;
65
66
    /** @var array<int,Interfaces\AuthenticatorInterface> */
67
    private array $authenticators;
68
69
    /**
70
     * @param array<int,Interfaces\AuthenticatorInterface> $authenticators
71
     */
72
    public function __construct(
73
        array $authenticators,
74
        TokenStorageInterface $tokenStorage,
75
        AccessDecisionManagerInterface $accessDecisionManager,
76
        UserCheckerInterface $userChecker = null,
77
        AbstractRequestRateLimiter $limiter = null,
78
        EventDispatcherInterface $eventDispatcher = null,
79
        PropertyAccessorInterface $propertyAccessor = null,
80
        bool $hideUserNotFoundExceptions = true,
81
        bool $eraseCredentials = true,
82
        string $firewallName = 'main'
83
    ) {
84
        $this->firewallName = $firewallName;
85
        $this->authenticators = $authenticators;
86
        $this->tokenStorage = $tokenStorage;
87
        $this->accessDecisionManager = $accessDecisionManager;
88
        $this->userChecker = $userChecker ?? new InMemoryUserChecker();
89
        $this->limiter = $limiter;
90
        $this->eventDispatcher = $eventDispatcher;
91
        $this->propertyAccessor = $propertyAccessor;
92
        $this->eraseCredentials = $eraseCredentials;
93
        $this->hideUserNotFoundExceptions = $hideUserNotFoundExceptions;
94
    }
95
96
    public function add(AuthenticatorInterface $authenticator): void
97
    {
98
        $this->authenticators[\get_class($authenticator)] = $authenticator;
99
    }
100
101
    public function has(string $authenticatorClass): bool
102
    {
103
        return isset($this->authenticators[$authenticatorClass]);
104
    }
105
106
    public function remove(string $authenticatorClass): void
107
    {
108
        unset($this->authenticators[$authenticatorClass]);
109
    }
110
111
    public function getTokenStorage(): TokenStorageInterface
112
    {
113
        return $this->tokenStorage;
114
    }
115
116
    /**
117
     * Returns the user(s) representation.
118
     *
119
     * @return UserInterface|array<int,UserInterface>|null
120
     */
121
    public function getUser(bool $current = true)
122
    {
123
        $token = $this->getToken($current);
124
125
        if (!\is_array($token)) {
0 ignored issues
show
introduced by
The condition is_array($token) is always false.
Loading history...
126
            return null !== $token ? $token->getUser() : $token;
127
        }
128
        $users = [];
129
130
        foreach ($token as $tk) {
131
            $users[] = $tk->getUser();
132
        }
133
134
        return $users;
135
    }
136
137
    /**
138
     * Returns the current security token(s).
139
     *
140
     * @return TokenInterface|array<int,TokenInterface>|null
141
     */
142
    public function getToken(bool $current = true)
143
    {
144
        $token = $this->tokenStorage->getToken();
145
146
        if ($current) {
147
            return $token;
148
        }
149
        $tokens = [];
150
        $tokenExist = -1;
151
152
        do {
153
            if (-1 === $tokenExist || $token !== $tokens[$tokenExist]) {
154
                $tokens[++$tokenExist] = $token;
155
            }
156
157
            if ($token instanceof SwitchUserToken) {
158
                $tokens[++$tokenExist] = $token = $token->getOriginalToken();
159
            }
160
        } while ($token instanceof SwitchUserToken);
161
162
        return \array_filter($tokens);
163
    }
164
165
    /**
166
     * Convenience method to programmatically authenticate a user and return
167
     * true if any success, else an exception or response on failure.
168
     *
169
     * @param array<int,string> $credentials The credentials to use
170
     * @param array<int,string> $onlyCheck   The class names list of authenticators to check
171
     *
172
     * @throws AuthenticationException if the authentication fails or credentials are invalid
173
     * @throws TooManyLoginAttemptsAuthenticationException if the authentication fails because of too many attempts
174
     *
175
     * @return ResponseInterface|true The response of the authentication
176
     */
177
    public function authenticate(ServerRequestInterface $request, array $credentials, array $onlyCheck = [])
178
    {
179
        if (empty($authenticators = $this->authenticators)) {
180
            throw new ProviderNotFoundException('No authenticator found.');
181
        }
182
183
        $previousToken = $this->tokenStorage->getToken();
184
        $credentials = Helper::getParameterValues($request, $credentials, $this->propertyAccessor);
185
186
        if (null !== $this->limiter) {
187
            $limit = $this->limiter->consume($request);
188
189
            if (!$limit->isAccepted()) {
190
                throw new TooManyLoginAttemptsAuthenticationException((int) \ceil(($limit->getRetryAfter()->getTimestamp() - \time()) / 60));
191
            }
192
        }
193
194
        foreach ($authenticators as $offset => $authenticator) {
195
            if (!empty($onlyCheck) && !\in_array($offset, $onlyCheck, true)) {
196
                continue;
197
            }
198
199
            if ($authenticator instanceof RequireTokenInterface) {
200
                $authenticator->setToken($previousToken);
201
            }
202
203
            if (!$authenticator->supports($request)) {
204
                continue;
205
            }
206
207
            try {
208
                $token = $this->executeAuthenticator($authenticator, $request, $credentials);
209
210
                if (null !== $token) {
211
                    $this->tokenStorage->setToken($token);
212
                }
213
            } catch (AuthenticationException $e) {
214
                // Avoid leaking error details in case of invalid user (e.g. user not found or invalid account status)
215
                // to prevent user enumeration via response content comparison
216
                if ($this->hideUserNotFoundExceptions && ($e instanceof UserNotFoundException || ($e instanceof AccountStatusException && !$e instanceof CustomUserMessageAccountStatusException))) {
217
                    $e = new BadCredentialsException('Bad credentials.', 0, $e);
218
                }
219
220
                $response = $authenticator instanceof FailureHandlerInterface ? $authenticator->failure($request, $e) : null;
221
222
                if (null !== $this->eventDispatcher) {
223
                    $this->eventDispatcher->dispatch($event = new AuthenticationFailureEvent($e, $authenticator, $request, $response));
224
                    $response = $event->getResponse();
225
                    $e = $event->getException();
226
                }
227
228
                if (!$response instanceof ResponseInterface) {
229
                    throw $e;
230
                }
231
232
                return $response;
233
            }
234
        }
235
236
        return true;
237
    }
238
239
    /**
240
     * {@inheritdoc}
241
     *
242
     * Checks if the attributes are granted against the current authentication token and optionally supplied subject.
243
     *
244
     * @throws AuthenticationCredentialsNotFoundException when the token storage has no authentication token and $exceptionOnNoToken is set to true
245
     */
246
    public function isGranted($attribute, $subject = null): bool
247
    {
248
        $token = $this->tokenStorage->getToken();
249
250
        if (!$token || !$token->getUser()) {
251
            $token = new NullToken();
252
        }
253
254
        return $this->accessDecisionManager->decide($token, [$attribute], $subject);
255
    }
256
257
    protected function executeAuthenticator(AuthenticatorInterface $authenticator, ServerRequestInterface $request, array $credentials): ?TokenInterface
258
    {
259
        $token = $authenticator->authenticate($request, $credentials, $this->firewallName);
260
261
        if (null !== $token) {
262
            if (null !== $this->limiter) {
263
                $this->limiter->reset($request);
264
            }
265
266
            if (!$token instanceof PreAuthenticatedToken) {
267
                $this->userChecker->checkPreAuth($token->getUser());
0 ignored issues
show
Bug introduced by
It seems like $token->getUser() can also be of type null; however, parameter $user of Symfony\Component\Securi...terface::checkPreAuth() does only seem to accept Symfony\Component\Security\Core\User\UserInterface, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

267
                $this->userChecker->checkPreAuth(/** @scrutinizer ignore-type */ $token->getUser());
Loading history...
268
            }
269
270
            if (null !== $this->eventDispatcher) {
271
                $this->eventDispatcher->dispatch($event = new AuthenticationSuccessEvent($token));
272
                $token = $event->getAuthenticationToken();
273
            }
274
275
            $this->userChecker->checkPostAuth($token->getUser());
0 ignored issues
show
Bug introduced by
It seems like $token->getUser() can also be of type null; however, parameter $user of Symfony\Component\Securi...erface::checkPostAuth() does only seem to accept Symfony\Component\Security\Core\User\UserInterface, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

275
            $this->userChecker->checkPostAuth(/** @scrutinizer ignore-type */ $token->getUser());
Loading history...
276
277
            if ($this->eraseCredentials) {
278
                $token->eraseCredentials();
279
            }
280
        }
281
282
        return $token;
283
    }
284
}
285