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

Authenticator::remove()   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
c 1
b 0
f 0
nc 1
nop 1
dl 0
loc 3
ccs 0
cts 2
cp 0
crap 2
rs 10
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