Passed
Push — master ( 3c8625...c93166 )
by Divine Niiquaye
12:46
created

Authenticator::add()   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\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 getTokenStorage(): TokenStorageInterface
100
    {
101
        return $this->tokenStorage;
102
    }
103
104
    /**
105
     * Returns the user(s) representation.
106
     *
107
     * @return UserInterface|array<int,UserInterface>|null
108
     */
109
    public function getUser(bool $current = true)
110
    {
111
        $token = $this->getToken($current);
112
113
        if (!\is_array($token)) {
0 ignored issues
show
introduced by
The condition is_array($token) is always false.
Loading history...
114
            return null !== $token ? $token->getUser() : $token;
115
        }
116
        $users = [];
117
118
        foreach ($token as $tk) {
119
            $users[] = $tk->getUser();
120
        }
121
122
        return $users;
123
    }
124
125
    /**
126
     * Returns the current security token(s).
127
     *
128
     * @return TokenInterface|array<int,TokenInterface>|null
129
     */
130
    public function getToken(bool $current = true)
131
    {
132
        $token = $this->tokenStorage->getToken();
133
134
        if ($current) {
135
            return $token;
136
        }
137
        $tokens = [];
138
        $tokenExist = -1;
139
140
        do {
141
            if (-1 === $tokenExist || $token !== $tokens[$tokenExist]) {
142
                $tokens[++$tokenExist] = $token;
143
            }
144
145
            if ($token instanceof SwitchUserToken) {
146
                $tokens[++$tokenExist] = $token = $token->getOriginalToken();
147
            }
148
        } while ($token instanceof SwitchUserToken);
149
150
        return \array_filter($tokens);
151
    }
152
153
    /**
154
     * Convenience method to programmatically authenticate a user and return
155
     * true if any success or a Response on failure.
156
     *
157
     * @param array<int,string> $credentials The credentials to use
158
     *
159
     * @throw AuthenticationException if the authentication fails
160
     *
161
     * @return ResponseInterface|bool The response of the authentication
162
     */
163
    public function authenticate(ServerRequestInterface $request, array $credentials)
164
    {
165
        $previousToken = $this->tokenStorage->getToken();
166
        $credentials = Helper::getParameterValues($request, $credentials, $this->propertyAccessor);
167
168
        if ($throttling = (null !== $this->limiter && $request instanceof Request)) {
169
            $limit = $this->limiter->consume($request->getRequest());
170
171
            if (!$limit->isAccepted()) {
172
                throw new TooManyLoginAttemptsAuthenticationException((int) \ceil(($limit->getRetryAfter()->getTimestamp() - \time()) / 60));
173
            }
174
        }
175
176
        foreach ($this->authenticators as $authenticator) {
177
            $authenticator->setToken($previousToken);
178
179
            if (!$authenticator->supports($request)) {
180
                continue;
181
            }
182
183
            try {
184
                if (null === $token = $authenticator->authenticate($request, $credentials)) {
185
                    continue; // Allow an authenticator without a token.
186
                }
187
188
                if (!$token instanceof PreAuthenticatedToken) {
189
                    $this->userChecker->checkPreAuth($token->getUser());
190
                }
191
192
                if (null !== $this->eventDispatcher) {
193
                    $this->eventDispatcher->dispatch($event = new AuthenticationSuccessEvent($token));
194
                    $token = $event->getAuthenticationToken();
195
                }
196
197
                if ($throttling) {
198
                    $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

198
                    $this->limiter->/** @scrutinizer ignore-call */ 
199
                                    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

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