Passed
Push — master ( cc80b9...1640e1 )
by Divine Niiquaye
12:24
created

Authenticator::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 18
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 8
nc 1
nop 8
dl 0
loc 18
ccs 0
cts 9
cp 0
crap 2
rs 10
c 1
b 0
f 0

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\Http\Request;
22
use Biurad\Security\Event\AuthenticationFailureEvent;
23
use Biurad\Security\Interfaces\AuthenticatorInterface;
24
use Psr\EventDispatcher\EventDispatcherInterface;
25
use Psr\Http\Message\ResponseInterface;
26
use Psr\Http\Message\ServerRequestInterface;
27
use Symfony\Component\HttpFoundation\RateLimiter\RequestRateLimiterInterface;
28
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
29
use Symfony\Component\Security\Core\Authentication\Token\NullToken;
30
use Symfony\Component\Security\Core\Authentication\Token\PreAuthenticatedToken;
31
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
32
use Symfony\Component\Security\Core\Authentication\Token\SwitchUserToken;
33
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
34
use Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface;
35
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
36
use Symfony\Component\Security\Core\Event\AuthenticationSuccessEvent;
37
use Symfony\Component\Security\Core\Exception\AccountStatusException;
38
use Symfony\Component\Security\Core\Exception\AuthenticationCredentialsNotFoundException;
39
use Symfony\Component\Security\Core\Exception\AuthenticationException;
40
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
41
use Symfony\Component\Security\Core\Exception\CustomUserMessageAccountStatusException;
42
use Symfony\Component\Security\Core\Exception\TooManyLoginAttemptsAuthenticationException;
43
use Symfony\Component\Security\Core\Exception\UserNotFoundException;
44
use Symfony\Component\Security\Core\User\InMemoryUserChecker;
45
use Symfony\Component\Security\Core\User\UserCheckerInterface;
46
use Symfony\Component\Security\Core\User\UserInterface;
47
48
/**
49
 * Authenticate a user with a set of authenticators.
50
 *
51
 * @author Divine Niiquaye Ibok <[email protected]>
52
 */
53
class Authenticator implements AuthorizationCheckerInterface
54
{
55
    private TokenStorageInterface $tokenStorage;
56
    private AccessDecisionManagerInterface $accessDecisionManager;
57
    private UserCheckerInterface $userChecker;
58
    private ?PropertyAccessorInterface $propertyAccessor;
59
    private ?RequestRateLimiterInterface $limiter;
60
    private ?EventDispatcherInterface $eventDispatcher;
61
    private bool $hideUserNotFoundExceptions;
62
63
    /** @var array<int,Interfaces\AuthenticatorInterface> */
64
    private array $authenticators;
65
66
    /**
67
     * @param array<int,Interfaces\AuthenticatorInterface> $authenticators
68
     */
69
    public function __construct(
70
        array $authenticators,
71
        TokenStorageInterface $tokenStorage,
72
        AccessDecisionManagerInterface $accessDecisionManager,
73
        UserCheckerInterface $userChecker = null,
74
        RequestRateLimiterInterface $limiter = null,
75
        EventDispatcherInterface $eventDispatcher = null,
76
        PropertyAccessorInterface $propertyAccessor = null,
77
        bool $hideUserNotFoundExceptions = true
78
    ) {
79
        $this->authenticators = $authenticators;
80
        $this->tokenStorage = $tokenStorage;
81
        $this->accessDecisionManager = $accessDecisionManager;
82
        $this->userChecker = $userChecker ?? new InMemoryUserChecker();
83
        $this->limiter = $limiter;
84
        $this->eventDispatcher = $eventDispatcher;
85
        $this->propertyAccessor = $propertyAccessor;
86
        $this->hideUserNotFoundExceptions = $hideUserNotFoundExceptions;
87
    }
88
89
    public function add(AuthenticatorInterface $authenticator): void
90
    {
91
        $this->authenticators[\get_class($authenticator)] = $authenticator;
92
    }
93
94
    public function has(string $authenticatorClass): bool
95
    {
96
        return isset($this->authenticators[$authenticatorClass]);
97
    }
98
99
    public function remove(string $authenticatorClass): void
100
    {
101
        unset($this->authenticators[$authenticatorClass]);
102
    }
103
104
    public function getTokenStorage(): TokenStorageInterface
105
    {
106
        return $this->tokenStorage;
107
    }
108
109
    /**
110
     * Returns the user(s) representation.
111
     *
112
     * @return UserInterface|array<int,UserInterface>|null
113
     */
114
    public function getUser(bool $current = true)
115
    {
116
        $token = $this->getToken($current);
117
118
        if (!\is_array($token)) {
0 ignored issues
show
introduced by
The condition is_array($token) is always false.
Loading history...
119
            return null !== $token ? $token->getUser() : $token;
120
        }
121
        $users = [];
122
123
        foreach ($token as $tk) {
124
            $users[] = $tk->getUser();
125
        }
126
127
        return $users;
128
    }
129
130
    /**
131
     * Returns the current security token(s).
132
     *
133
     * @return TokenInterface|array<int,TokenInterface>|null
134
     */
135
    public function getToken(bool $current = true)
136
    {
137
        $token = $this->tokenStorage->getToken();
138
139
        if ($current) {
140
            return $token;
141
        }
142
        $tokens = [];
143
        $tokenExist = -1;
144
145
        do {
146
            if (-1 === $tokenExist || $token !== $tokens[$tokenExist]) {
147
                $tokens[++$tokenExist] = $token;
148
            }
149
150
            if ($token instanceof SwitchUserToken) {
151
                $tokens[++$tokenExist] = $token = $token->getOriginalToken();
152
            }
153
        } while ($token instanceof SwitchUserToken);
154
155
        return \array_filter($tokens);
156
    }
157
158
    /**
159
     * Convenience method to programmatically authenticate a user and return
160
     * true if any success, else an exception or response on failure.
161
     *
162
     * @param array<int,string> $credentials The credentials to use
163
     * @param array<int,string> $onlyCheck   The class names list of authenticators to check
164
     *
165
     * @throws AuthenticationException if the authentication fails or credentials are invalid
166
     * @throws TooManyLoginAttemptsAuthenticationException if the authentication fails because of too many attempts
167
     *
168
     * @return ResponseInterface|true The response of the authentication
169
     */
170
    public function authenticate(ServerRequestInterface $request, array $credentials, array $onlyCheck = [])
171
    {
172
        $previousToken = $this->tokenStorage->getToken();
173
        $credentials = Helper::getParameterValues($request, $credentials, $this->propertyAccessor);
174
175
        if ($throttling = (null !== $this->limiter && $request instanceof Request)) {
176
            $limit = $this->limiter->consume($request->getRequest());
177
178
            if (!$limit->isAccepted()) {
179
                throw new TooManyLoginAttemptsAuthenticationException((int) \ceil(($limit->getRetryAfter()->getTimestamp() - \time()) / 60));
180
            }
181
        }
182
183
        foreach ($this->authenticators as $offset => $authenticator) {
184
            if (!empty($onlyCheck) && !\in_array($offset, $onlyCheck, true)) {
185
                continue;
186
            }
187
            $authenticator->setToken($previousToken);
188
189
            if (!$authenticator->supports($request)) {
190
                continue;
191
            }
192
193
            try {
194
                if (null === $token = $authenticator->authenticate($request, $credentials)) {
195
                    continue; // Allow an authenticator without a token.
196
                }
197
198
                if (!$token instanceof PreAuthenticatedToken) {
199
                    $this->userChecker->checkPreAuth($token->getUser());
200
                }
201
202
                if (null !== $this->eventDispatcher) {
203
                    $this->eventDispatcher->dispatch($event = new AuthenticationSuccessEvent($token));
204
                    $token = $event->getAuthenticationToken();
205
                }
206
207
                if ($throttling) {
208
                    $this->limiter->reset($request->getRequest());
0 ignored issues
show
Bug introduced by
The method reset() does not exist on null. ( Ignorable by Annotation )

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

208
                    $this->limiter->/** @scrutinizer ignore-call */ 
209
                                    reset($request->getRequest());

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
Bug introduced by
The method getRequest() does not exist on Psr\Http\Message\ServerRequestInterface. Did you maybe mean getRequestTarget()? ( Ignorable by Annotation )

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

208
                    $this->limiter->reset($request->/** @scrutinizer ignore-call */ getRequest());

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
209
                }
210
211
                if ($token !== $previousToken) {
212
                    $this->tokenStorage->setToken($token);
213
                }
214
215
                $this->userChecker->checkPostAuth($token->getUser());
216
                break;
217
            } catch (AuthenticationException $e) {
218
                // Avoid leaking error details in case of invalid user (e.g. user not found or invalid account status)
219
                // to prevent user enumeration via response content comparison
220
                if ($this->hideUserNotFoundExceptions && ($e instanceof UserNotFoundException || ($e instanceof AccountStatusException && !$e instanceof CustomUserMessageAccountStatusException))) {
221
                    $e = new BadCredentialsException('Bad credentials.', 0, $e);
222
                }
223
224
                $response = $authenticator->failure($request, $e);
225
226
                if (null !== $this->eventDispatcher) {
227
                    $this->eventDispatcher->dispatch($event = new AuthenticationFailureEvent($e, $authenticator, $request, $response));
228
                    $response = $event->getResponse();
229
                    $e = $event->getException();
230
                }
231
232
                if (!$response instanceof ResponseInterface) {
233
                    throw $e;
234
                }
235
236
                return $response;
237
            }
238
        }
239
240
        return true;
241
    }
242
243
    /**
244
     * {@inheritdoc}
245
     *
246
     * Checks if the attributes are granted against the current authentication token and optionally supplied subject.
247
     *
248
     * @throws AuthenticationCredentialsNotFoundException when the token storage has no authentication token and $exceptionOnNoToken is set to true
249
     */
250
    public function isGranted($attribute, $subject = null): bool
251
    {
252
        $token = $this->tokenStorage->getToken();
253
254
        if (!$token || !$token->getUser()) {
255
            $token = new NullToken();
256
        }
257
258
        return $this->accessDecisionManager->decide($token, [$attribute], $subject);
259
    }
260
}
261