Authenticator::__construct()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 22
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 10
c 1
b 0
f 0
nc 1
nop 10
dl 0
loc 22
ccs 0
cts 11
cp 0
crap 2
rs 9.9332

How to fix   Many Parameters   

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

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