Passed
Pull Request — master (#1473)
by
unknown
35:03
created

AbstractGrant   B

Complexity

Total Complexity 50

Size/Duplication

Total Lines 376
Duplicated Lines 0 %

Test Coverage

Coverage 95.28%

Importance

Changes 9
Bugs 0 Features 0
Metric Value
eloc 107
dl 0
loc 376
ccs 101
cts 106
cp 0.9528
rs 8.4
c 9
b 0
f 0
wmc 50

26 Methods

Rating   Name   Duplication   Size   Complexity  
A getIntervalVisibility() 0 3 1
A respondToDeviceAuthorizationRequest() 0 3 1
A setScopeRepository() 0 3 1
A canRespondToAuthorizationRequest() 0 3 1
A supportsGrantType() 0 4 2
A revokeRefreshTokens() 0 3 1
A getClientEntityOrFail() 0 9 2
A setPrivateKey() 0 3 1
A canRespondToAccessTokenRequest() 0 7 2
A completeDeviceAuthorizationRequest() 0 3 1
A issueRefreshToken() 0 32 6
A setIncludeVerificationUriComplete() 0 3 1
A completeAuthorizationRequest() 0 3 1
A issueAuthCode() 0 37 6
A issueAccessToken() 0 27 4
A setUserRepository() 0 3 1
A setRefreshTokenTTL() 0 3 1
A setIntervalVisibility() 0 3 1
A setDefaultScope() 0 3 1
A validateScopes() 0 21 5
A setAuthCodeRepository() 0 3 1
A generateUniqueIdentifier() 0 14 4
A validateRedirectUri() 0 10 2
A convertScopesQueryStringToArray() 0 3 1
A canRespondToDeviceAuthorizationRequest() 0 3 1
A validateAuthorizationRequest() 0 3 1

How to fix   Complexity   

Complex Class

Complex classes like AbstractGrant often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use AbstractGrant, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/**
4
 * OAuth 2.0 Abstract grant.
5
 *
6
 * @author      Alex Bilbie <[email protected]>
7
 * @copyright   Copyright (c) Alex Bilbie
8
 * @license     http://mit-license.org/
9
 *
10
 * @link        https://github.com/thephpleague/oauth2-server
11
 */
12
13
declare(strict_types=1);
14
15
namespace League\OAuth2\Server\Grant;
16
17
use DateInterval;
18
use DateTimeImmutable;
19
use DomainException;
20
use Error;
21
use Exception;
22
use League\OAuth2\Server\AbstractHandler;
23
use League\OAuth2\Server\CryptKeyInterface;
24
use League\OAuth2\Server\Entities\AccessTokenEntityInterface;
25
use League\OAuth2\Server\Entities\AuthCodeEntityInterface;
26
use League\OAuth2\Server\Entities\ClientEntityInterface;
27
use League\OAuth2\Server\Entities\RefreshTokenEntityInterface;
28
use League\OAuth2\Server\Entities\ScopeEntityInterface;
29
use League\OAuth2\Server\Exception\OAuthServerException;
30
use League\OAuth2\Server\Exception\UniqueTokenIdentifierConstraintViolationException;
31
use League\OAuth2\Server\RedirectUriValidators\RedirectUriValidator;
32
use League\OAuth2\Server\Repositories\AuthCodeRepositoryInterface;
33
use League\OAuth2\Server\Repositories\ScopeRepositoryInterface;
34
use League\OAuth2\Server\Repositories\UserRepositoryInterface;
35
use League\OAuth2\Server\RequestEvent;
36
use League\OAuth2\Server\RequestTypes\AuthorizationRequestInterface;
37
use League\OAuth2\Server\ResponseTypes\DeviceCodeResponse;
38
use League\OAuth2\Server\ResponseTypes\ResponseTypeInterface;
39
use LogicException;
40
use Psr\Http\Message\ServerRequestInterface;
41
use TypeError;
42
43
use function array_filter;
44
use function array_key_exists;
45
use function bin2hex;
46
use function explode;
47
use function is_string;
48
use function random_bytes;
49
use function trim;
50
51
/**
52
 * Abstract grant class.
53
 */
54
abstract class AbstractGrant extends AbstractHandler implements GrantTypeInterface
55
{
56
    protected const SCOPE_DELIMITER_STRING = ' ';
57
58
    protected const MAX_RANDOM_TOKEN_GENERATION_ATTEMPTS = 10;
59
60
    protected ScopeRepositoryInterface $scopeRepository;
61
62
    protected AuthCodeRepositoryInterface $authCodeRepository;
63
64
    protected UserRepositoryInterface $userRepository;
65
66
    protected DateInterval $refreshTokenTTL;
67
68
    protected CryptKeyInterface $privateKey;
69
70
    protected string $defaultScope;
71
72
    protected bool $revokeRefreshTokens = true;
73
74
    public function setScopeRepository(ScopeRepositoryInterface $scopeRepository): void
75
    {
76
        $this->scopeRepository = $scopeRepository;
77
    }
78
79
    public function setAuthCodeRepository(AuthCodeRepositoryInterface $authCodeRepository): void
80
    {
81
        $this->authCodeRepository = $authCodeRepository;
82
    }
83
84
    public function setUserRepository(UserRepositoryInterface $userRepository): void
85
    {
86
        $this->userRepository = $userRepository;
87
    }
88
89 101
    /**
90
     * {@inheritdoc}
91 101
     */
92
    public function setRefreshTokenTTL(DateInterval $refreshTokenTTL): void
93
    {
94 55
        $this->refreshTokenTTL = $refreshTokenTTL;
95
    }
96 55
97
    /**
98
     * Set the private key
99 70
     */
100
    public function setPrivateKey(CryptKeyInterface $privateKey): void
101 70
    {
102
        $this->privateKey = $privateKey;
103
    }
104 94
105
    public function setDefaultScope(string $scope): void
106 94
    {
107
        $this->defaultScope = $scope;
108
    }
109 54
110
    public function revokeRefreshTokens(bool $willRevoke): void
111 54
    {
112
        $this->revokeRefreshTokens = $willRevoke;
113
    }
114 6
115
    /**
116 6
     * {@inheritdoc}
117
     */
118
    protected function getClientEntityOrFail(string $clientId, ServerRequestInterface $request): ClientEntityInterface
119
    {
120
        $client = parent::getClientEntityOrFail($clientId, $request);
121
122 3
        if ($this->supportsGrantType($client, $this->getIdentifier()) === false) {
123
            throw OAuthServerException::unauthorizedClient();
124 3
        }
125
126
        return $client;
127
    }
128
129
    /**
130 47
     * Returns true if the given client is authorized to use the given grant type.
131
     */
132 47
    protected function supportsGrantType(ClientEntityInterface $client, string $grantType): bool
133
    {
134
        return method_exists($client, 'supportsGrantType') === false
135 39
            || $client->supportsGrantType($grantType) === true;
136
    }
137 39
138
    /**
139
     * Validate redirectUri from the request. If a redirect URI is provided
140 12
     * ensure it matches what is pre-registered
141
     *
142 12
     * @throws OAuthServerException
143
     */
144
    protected function validateRedirectUri(
145
        string $redirectUri,
146
        ClientEntityInterface $client,
147
        ServerRequestInterface $request
148
    ): void {
149
        $validator = new RedirectUriValidator($client->getRedirectUri());
150 66
151
        if (!$validator->validateRedirectUri($redirectUri)) {
152 66
            $this->getEmitter()->emit(new RequestEvent(RequestEvent::CLIENT_AUTHENTICATION_FAILED, $request));
153
            throw OAuthServerException::invalidClient($request);
154 63
        }
155
    }
156 55
157 16
    /**
158 4
     * Validate scopes in the request.
159
     *
160
     * @param null|string|string[] $scopes
161 12
     *
162
     * @throws OAuthServerException
163
     *
164
     * @return ScopeEntityInterface[]
165
     */
166
    public function validateScopes(string|array|null $scopes, ?string $redirectUri = null): array
167
    {
168 51
        if ($scopes === null) {
0 ignored issues
show
introduced by
The condition $scopes === null is always false.
Loading history...
169
            $scopes = [];
170
        } elseif (is_string($scopes)) {
0 ignored issues
show
introduced by
The condition is_string($scopes) is always false.
Loading history...
171
            $scopes = $this->convertScopesQueryStringToArray($scopes);
172
        }
173
174
        $validScopes = [];
175
176
        foreach ($scopes as $scopeItem) {
177
            $scope = $this->scopeRepository->getScopeEntityByIdentifier($scopeItem);
178
179
            if ($scope instanceof ScopeEntityInterface === false) {
180
                throw OAuthServerException::invalidScope($scopeItem, $redirectUri);
181
            }
182
183 91
            $validScopes[] = $scope;
184
        }
185 91
186
        return $validScopes;
187 91
    }
188 11
189 11
    /**
190
     * Converts a scopes query string to an array to easily iterate for validation.
191
     *
192 80
     * @return string[]
193 1
     */
194
    private function convertScopesQueryStringToArray(string $scopes): array
195
    {
196 79
        return array_filter(explode(self::SCOPE_DELIMITER_STRING, trim($scopes)), static fn ($scope) => $scope !== '');
197
    }
198
199
    /**
200
     * Issue an access token.
201
     *
202 83
     * @param ScopeEntityInterface[] $scopes
203
     *
204 83
     * @throws OAuthServerException
205 83
     * @throws UniqueTokenIdentifierConstraintViolationException
206
     */
207
    protected function issueAccessToken(
208
        DateInterval $accessTokenTTL,
209
        ClientEntityInterface $client,
210
        string|null $userIdentifier,
211
        array $scopes = []
212
    ): AccessTokenEntityInterface {
213
        $maxGenerationAttempts = self::MAX_RANDOM_TOKEN_GENERATION_ATTEMPTS;
214
215
        $accessToken = $this->accessTokenRepository->getNewToken($client, $scopes, $userIdentifier);
216 67
        $accessToken->setExpiryDateTime((new DateTimeImmutable())->add($accessTokenTTL));
217
        $accessToken->setPrivateKey($this->privateKey);
218 67
219
        while ($maxGenerationAttempts-- > 0) {
220 67
            $accessToken->setIdentifier($this->generateUniqueIdentifier());
221
            try {
222 67
                $this->accessTokenRepository->persistNewAccessToken($accessToken);
223 3
224
                return $accessToken;
225
            } catch (UniqueTokenIdentifierConstraintViolationException $e) {
226 64
                if ($maxGenerationAttempts === 0) {
227
                    throw $e;
228 63
                }
229
            }
230
        }
231
232
        // This should never be hit. It is here to work around a PHPStan false error
233
        return $accessToken;
234
    }
235
236
    /**
237 17
     * Issue an auth code.
238
     *
239
     * @param non-empty-string       $userIdentifier
0 ignored issues
show
Documentation Bug introduced by
The doc comment non-empty-string at position 0 could not be parsed: Unknown type name 'non-empty-string' at position 0 in non-empty-string.
Loading history...
240
     * @param ScopeEntityInterface[] $scopes
241
     *
242 17
     * @throws OAuthServerException
243
     * @throws UniqueTokenIdentifierConstraintViolationException
244 17
     */
245 4
    protected function issueAuthCode(
246 4
        DateInterval $authCodeTTL,
247
        ClientEntityInterface $client,
248
        string $userIdentifier,
249
        ?string $redirectUri,
250
        array $scopes = []
251
    ): AuthCodeEntityInterface {
252
        $maxGenerationAttempts = self::MAX_RANDOM_TOKEN_GENERATION_ATTEMPTS;
253
254
        $authCode = $this->authCodeRepository->getNewAuthCode();
255
        $authCode->setExpiryDateTime((new DateTimeImmutable())->add($authCodeTTL));
256
        $authCode->setClient($client);
257
        $authCode->setUserIdentifier($userIdentifier);
258
259 52
        if ($redirectUri !== null) {
260
            $authCode->setRedirectUri($redirectUri);
261 52
        }
262
263 52
        foreach ($scopes as $scope) {
264 37
            $authCode->addScope($scope);
265
        }
266
267 52
        while ($maxGenerationAttempts-- > 0) {
268
            $authCode->setIdentifier($this->generateUniqueIdentifier());
269 52
            try {
270 52
                $this->authCodeRepository->persistNewAuthCode($authCode);
271
272 52
                return $authCode;
273 7
            } catch (UniqueTokenIdentifierConstraintViolationException $e) {
274
                if ($maxGenerationAttempts === 0) {
275
                    throw $e;
276 45
                }
277
            }
278
        }
279 45
280
        // This should never be hit. It is here to work around a PHPStan false error
281
        return $authCode;
282
    }
283
284
    /**
285
     * @throws OAuthServerException
286
     * @throws UniqueTokenIdentifierConstraintViolationException
287 37
     */
288
    protected function issueRefreshToken(AccessTokenEntityInterface $accessToken): ?RefreshTokenEntityInterface
289 37
    {
290
        if ($this->supportsGrantType($accessToken->getClient(), 'refresh_token') === false) {
291
            return null;
292
        }
293
294
        $refreshToken = $this->refreshTokenRepository->getNewRefreshToken();
295
296
        if ($refreshToken === null) {
297
            return null;
298
        }
299
300
        $refreshToken->setExpiryDateTime((new DateTimeImmutable())->add($this->refreshTokenTTL));
301 100
        $refreshToken->setAccessToken($accessToken);
302
303 100
        $maxGenerationAttempts = self::MAX_RANDOM_TOKEN_GENERATION_ATTEMPTS;
304
305 100
        while ($maxGenerationAttempts-- > 0) {
306 100
            $refreshToken->setIdentifier($this->generateUniqueIdentifier());
307
            try {
308 2
                $this->refreshTokenRepository->persistNewRefreshToken($refreshToken);
309
310
                return $refreshToken;
311 100
            } catch (UniqueTokenIdentifierConstraintViolationException $e) {
312 80
                if ($maxGenerationAttempts === 0) {
313
                    throw $e;
314 80
                }
315
            }
316
        }
317
318
        // This should never be hit. It is here to work around a PHPStan false error
319 100
        return $refreshToken;
320
    }
321
322
    /**
323
     * Generate a new unique identifier.
324
     *
325
     * @return non-empty-string
0 ignored issues
show
Documentation Bug introduced by
The doc comment non-empty-string at position 0 could not be parsed: Unknown type name 'non-empty-string' at position 0 in non-empty-string.
Loading history...
326
     *
327
     * @throws OAuthServerException
328
     */
329 75
    protected function generateUniqueIdentifier(int $length = 40): string
330
    {
331 75
        try {
332
            if ($length < 1) {
333
                throw new DomainException('Length must be a positive integer');
334
            }
335
336
            return bin2hex(random_bytes($length));
337
            // @codeCoverageIgnoreStart
338
        } catch (TypeError | Error $e) {
339
            throw OAuthServerException::serverError('An unexpected error has occurred', $e);
340
        } catch (Exception $e) {
341
            // If you get this message, the CSPRNG failed hard.
342
            throw OAuthServerException::serverError('Could not generate a random string', $e);
343 73
        }
344
        // @codeCoverageIgnoreEnd
345 73
    }
346 63
347
    /**
348
     * {@inheritdoc}
349 10
     */
350 10
    public function canRespondToAccessTokenRequest(ServerRequestInterface $request): bool
351 3
    {
352
        $requestParameters = (array) $request->getParsedBody();
353
354 7
        return (
355
            array_key_exists('grant_type', $requestParameters)
356 7
            && $requestParameters['grant_type'] === $this->getIdentifier()
357 1
        );
358
    }
359
360 6
    /**
361 2
     * {@inheritdoc}
362
     */
363
    public function canRespondToAuthorizationRequest(ServerRequestInterface $request): bool
364 4
    {
365
        return false;
366 4
    }
367
368
    /**
369
     * {@inheritdoc}
370 4
     */
371
    public function validateAuthorizationRequest(ServerRequestInterface $request): AuthorizationRequestInterface
372
    {
373
        throw new LogicException('This grant cannot validate an authorization request');
374
    }
375
376
    /**
377
     * {@inheritdoc}
378
     */
379
    public function completeAuthorizationRequest(AuthorizationRequestInterface $authorizationRequest): ResponseTypeInterface
380 24
    {
381
        throw new LogicException('This grant cannot complete an authorization request');
382 24
    }
383
384
    /**
385
     * {@inheritdoc}
386
     */
387
    public function canRespondToDeviceAuthorizationRequest(ServerRequestInterface $request): bool
388
    {
389
        return false;
390
    }
391
392 1
    /**
393
     * {@inheritdoc}
394 1
     */
395
    public function respondToDeviceAuthorizationRequest(ServerRequestInterface $request): DeviceCodeResponse
396
    {
397
        throw new LogicException('This grant cannot validate a device authorization request');
398
    }
399
400
    /**
401
     * {@inheritdoc}
402
     */
403
    public function completeDeviceAuthorizationRequest(string $deviceCode, string $userId, bool $userApproved): void
404 31
    {
405
        throw new LogicException('This grant cannot complete a device authorization request');
406 31
    }
407
408
    /**
409
     * {@inheritdoc}
410
     */
411
    public function setIntervalVisibility(bool $intervalVisibility): void
412
    {
413
        throw new LogicException('This grant does not support the interval parameter');
414
    }
415
416
    /**
417 28
     * {@inheritdoc}
418
     */
419
    public function getIntervalVisibility(): bool
420
    {
421
        return false;
422
    }
423 28
424
    /**
425 28
     * {@inheritdoc}
426 28
     */
427 28
    public function setIncludeVerificationUriComplete(bool $includeVerificationUriComplete): void
428
    {
429 28
        throw new LogicException('This grant does not support the verification_uri_complete parameter');
430 28
    }
431
}
432