Completed
Push — master ( c7f499...382b6f )
by Andrew
12s queued 10s
created

AbstractGrant   F

Complexity

Total Complexity 60

Size/Duplication

Total Lines 524
Duplicated Lines 0 %

Coupling/Cohesion

Components 3
Dependencies 12

Test Coverage

Coverage 97.97%

Importance

Changes 0
Metric Value
wmc 60
lcom 3
cbo 12
dl 0
loc 524
ccs 145
cts 148
cp 0.9797
rs 3.6
c 0
b 0
f 0

26 Methods

Rating   Name   Duplication   Size   Complexity  
A setClientRepository() 0 4 1
A setAccessTokenRepository() 0 4 1
A setScopeRepository() 0 4 1
A setRefreshTokenRepository() 0 4 1
A setAuthCodeRepository() 0 4 1
A setUserRepository() 0 4 1
A setRefreshTokenTTL() 0 4 1
A setPrivateKey() 0 4 1
A setDefaultScope() 0 4 1
A validateRedirectUri() 0 17 5
A validateScopes() 0 20 4
A convertScopesQueryStringToArray() 0 6 1
A getRequestParameter() 0 6 1
A getBasicAuthCredentials() 0 21 5
A getQueryStringParameter() 0 4 2
A getCookieParameter() 0 4 2
A getServerParameter() 0 4 2
A issueAccessToken() 0 30 5
B issueAuthCode() 0 35 6
A validateClient() 0 32 4
A issueRefreshToken() 0 26 5
A generateUniqueIdentifier() 0 15 4
A canRespondToAccessTokenRequest() 0 9 2
A canRespondToAuthorizationRequest() 0 4 1
A validateAuthorizationRequest() 0 4 1
A completeAuthorizationRequest() 0 4 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. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

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
 * OAuth 2.0 Abstract grant.
4
 *
5
 * @author      Alex Bilbie <[email protected]>
6
 * @copyright   Copyright (c) Alex Bilbie
7
 * @license     http://mit-license.org/
8
 *
9
 * @link        https://github.com/thephpleague/oauth2-server
10
 */
11
namespace League\OAuth2\Server\Grant;
12
13
use DateInterval;
14
use DateTime;
15
use Error;
16
use Exception;
17
use League\Event\EmitterAwareTrait;
18
use League\OAuth2\Server\CryptKey;
19
use League\OAuth2\Server\CryptTrait;
20
use League\OAuth2\Server\Entities\AccessTokenEntityInterface;
21
use League\OAuth2\Server\Entities\AuthCodeEntityInterface;
22
use League\OAuth2\Server\Entities\ClientEntityInterface;
23
use League\OAuth2\Server\Entities\RefreshTokenEntityInterface;
24
use League\OAuth2\Server\Entities\ScopeEntityInterface;
25
use League\OAuth2\Server\Exception\OAuthServerException;
26
use League\OAuth2\Server\Exception\UniqueTokenIdentifierConstraintViolationException;
27
use League\OAuth2\Server\Repositories\AccessTokenRepositoryInterface;
28
use League\OAuth2\Server\Repositories\AuthCodeRepositoryInterface;
29
use League\OAuth2\Server\Repositories\ClientRepositoryInterface;
30
use League\OAuth2\Server\Repositories\RefreshTokenRepositoryInterface;
31
use League\OAuth2\Server\Repositories\ScopeRepositoryInterface;
32
use League\OAuth2\Server\Repositories\UserRepositoryInterface;
33
use League\OAuth2\Server\RequestEvent;
34
use League\OAuth2\Server\RequestTypes\AuthorizationRequest;
35
use LogicException;
36
use Psr\Http\Message\ServerRequestInterface;
37
use TypeError;
38
39
/**
40
 * Abstract grant class.
41
 */
42
abstract class AbstractGrant implements GrantTypeInterface
43
{
44
    use EmitterAwareTrait, CryptTrait;
45
46
    const SCOPE_DELIMITER_STRING = ' ';
47
48
    const MAX_RANDOM_TOKEN_GENERATION_ATTEMPTS = 10;
49
50
    /**
51
     * @var ClientRepositoryInterface
52
     */
53
    protected $clientRepository;
54
55
    /**
56
     * @var AccessTokenRepositoryInterface
57
     */
58
    protected $accessTokenRepository;
59
60
    /**
61
     * @var ScopeRepositoryInterface
62
     */
63
    protected $scopeRepository;
64
65
    /**
66
     * @var AuthCodeRepositoryInterface
67
     */
68
    protected $authCodeRepository;
69
70
    /**
71
     * @var RefreshTokenRepositoryInterface
72
     */
73
    protected $refreshTokenRepository;
74
75
    /**
76
     * @var UserRepositoryInterface
77
     */
78
    protected $userRepository;
79
80
    /**
81
     * @var DateInterval
82
     */
83
    protected $refreshTokenTTL;
84
85
    /**
86
     * @var CryptKey
87
     */
88
    protected $privateKey;
89
90
    /**
91
     * @string
92
     */
93
    protected $defaultScope;
94
95
    /**
96
     * @param ClientRepositoryInterface $clientRepository
97
     */
98 67
    public function setClientRepository(ClientRepositoryInterface $clientRepository)
99
    {
100 67
        $this->clientRepository = $clientRepository;
101 67
    }
102
103
    /**
104
     * @param AccessTokenRepositoryInterface $accessTokenRepository
105
     */
106 45
    public function setAccessTokenRepository(AccessTokenRepositoryInterface $accessTokenRepository)
107
    {
108 45
        $this->accessTokenRepository = $accessTokenRepository;
109 45
    }
110
111
    /**
112
     * @param ScopeRepositoryInterface $scopeRepository
113
     */
114 40
    public function setScopeRepository(ScopeRepositoryInterface $scopeRepository)
115
    {
116 40
        $this->scopeRepository = $scopeRepository;
117 40
    }
118
119
    /**
120
     * @param RefreshTokenRepositoryInterface $refreshTokenRepository
121
     */
122 60
    public function setRefreshTokenRepository(RefreshTokenRepositoryInterface $refreshTokenRepository)
123
    {
124 60
        $this->refreshTokenRepository = $refreshTokenRepository;
125 60
    }
126
127
    /**
128
     * @param AuthCodeRepositoryInterface $authCodeRepository
129
     */
130 43
    public function setAuthCodeRepository(AuthCodeRepositoryInterface $authCodeRepository)
131
    {
132 43
        $this->authCodeRepository = $authCodeRepository;
133 43
    }
134
135
    /**
136
     * @param UserRepositoryInterface $userRepository
137
     */
138 6
    public function setUserRepository(UserRepositoryInterface $userRepository)
139
    {
140 6
        $this->userRepository = $userRepository;
141 6
    }
142
143
    /**
144
     * {@inheritdoc}
145
     */
146 2
    public function setRefreshTokenTTL(DateInterval $refreshTokenTTL)
147
    {
148 2
        $this->refreshTokenTTL = $refreshTokenTTL;
149 2
    }
150
151
    /**
152
     * Set the private key
153
     *
154
     * @param CryptKey $key
155
     */
156 21
    public function setPrivateKey(CryptKey $key)
157
    {
158 21
        $this->privateKey = $key;
159 21
    }
160
161
    /**
162
     * @param string $scope
163
     */
164 17
    public function setDefaultScope($scope)
165
    {
166 17
        $this->defaultScope = $scope;
167 17
    }
168
169
    /**
170
     * Validate the client.
171
     *
172
     * @param ServerRequestInterface $request
173
     *
174
     * @throws OAuthServerException
175
     *
176
     * @return ClientEntityInterface
177
     */
178 45
    protected function validateClient(ServerRequestInterface $request)
179
    {
180 45
        list($basicAuthUser, $basicAuthPassword) = $this->getBasicAuthCredentials($request);
181
182 45
        $clientId = $this->getRequestParameter('client_id', $request, $basicAuthUser);
183 45
        if ($clientId === null) {
184 1
            throw OAuthServerException::invalidRequest('client_id');
185
        }
186
187
        // If the client is confidential require the client secret
188 44
        $clientSecret = $this->getRequestParameter('client_secret', $request, $basicAuthPassword);
189
190 44
        $client = $this->clientRepository->getClientEntity(
191 44
            $clientId,
192 44
            $this->getIdentifier(),
193
            $clientSecret,
194 44
            true
195
        );
196
197 44
        if ($client instanceof ClientEntityInterface === false) {
198 4
            $this->getEmitter()->emit(new RequestEvent(RequestEvent::CLIENT_AUTHENTICATION_FAILED, $request));
199 4
            throw OAuthServerException::invalidClient();
200
        }
201
202 40
        $redirectUri = $this->getRequestParameter('redirect_uri', $request, null);
203
204 40
        if ($redirectUri !== null) {
205 21
            $this->validateRedirectUri($redirectUri, $client, $request);
206
        }
207
208 38
        return $client;
209
    }
210
211
    /**
212
     * Validate redirectUri from the request.
213
     * If a redirect URI is provided ensure it matches what is pre-registered
214
     *
215
     * @param string                 $redirectUri
216
     * @param ClientEntityInterface  $client
217
     * @param ServerRequestInterface $request
218
     *
219
     * @throws OAuthServerException
220
     */
221 35
    protected function validateRedirectUri(
222
        string $redirectUri,
223
        ClientEntityInterface $client,
224
        ServerRequestInterface $request
225
    ) {
226 35
        if (\is_string($client->getRedirectUri())
227 35
            && (strcmp($client->getRedirectUri(), $redirectUri) !== 0)
228
        ) {
229 3
            $this->getEmitter()->emit(new RequestEvent(RequestEvent::CLIENT_AUTHENTICATION_FAILED, $request));
230 3
            throw OAuthServerException::invalidClient();
231 32
        } elseif (\is_array($client->getRedirectUri())
232 32
            && \in_array($redirectUri, $client->getRedirectUri(), true) === false
233
        ) {
234 3
            $this->getEmitter()->emit(new RequestEvent(RequestEvent::CLIENT_AUTHENTICATION_FAILED, $request));
235 3
            throw OAuthServerException::invalidClient();
236
        }
237 29
    }
238
239
    /**
240
     * Validate scopes in the request.
241
     *
242
     * @param string|array $scopes
243
     * @param string       $redirectUri
244
     *
245
     * @throws OAuthServerException
246
     *
247
     * @return ScopeEntityInterface[]
248
     */
249 37
    public function validateScopes($scopes, $redirectUri = null)
250
    {
251 37
        if (!\is_array($scopes)) {
252 25
            $scopes = $this->convertScopesQueryStringToArray($scopes);
253
        }
254
255 37
        $validScopes = [];
256
257 37
        foreach ($scopes as $scopeItem) {
258 31
            $scope = $this->scopeRepository->getScopeEntityByIdentifier($scopeItem);
259
260 31
            if ($scope instanceof ScopeEntityInterface === false) {
261 1
                throw OAuthServerException::invalidScope($scopeItem, $redirectUri);
262
            }
263
264 30
            $validScopes[] = $scope;
265
        }
266
267 36
        return $validScopes;
268
    }
269
270
    /**
271
     * Converts a scopes query string to an array to easily iterate for validation.
272
     *
273
     * @param string $scopes
274
     *
275
     * @return array
276
     */
277 25
    private function convertScopesQueryStringToArray($scopes)
278
    {
279
        return array_filter(explode(self::SCOPE_DELIMITER_STRING, trim($scopes)), function ($scope) {
280 25
            return !empty($scope);
281 25
        });
282
    }
283
284
    /**
285
     * Retrieve request parameter.
286
     *
287
     * @param string                 $parameter
288
     * @param ServerRequestInterface $request
289
     * @param mixed                  $default
290
     *
291
     * @return null|string
292
     */
293 45
    protected function getRequestParameter($parameter, ServerRequestInterface $request, $default = null)
294
    {
295 45
        $requestParameters = (array) $request->getParsedBody();
296
297 45
        return $requestParameters[$parameter] ?? $default;
298
    }
299
300
    /**
301
     * Retrieve HTTP Basic Auth credentials with the Authorization header
302
     * of a request. First index of the returned array is the username,
303
     * second is the password (so list() will work). If the header does
304
     * not exist, or is otherwise an invalid HTTP Basic header, return
305
     * [null, null].
306
     *
307
     * @param ServerRequestInterface $request
308
     *
309
     * @return string[]|null[]
310
     */
311 50
    protected function getBasicAuthCredentials(ServerRequestInterface $request)
312
    {
313 50
        if (!$request->hasHeader('Authorization')) {
314 45
            return [null, null];
315
        }
316
317 5
        $header = $request->getHeader('Authorization')[0];
318 5
        if (strpos($header, 'Basic ') !== 0) {
319 1
            return [null, null];
320
        }
321
322 4
        if (!($decoded = base64_decode(substr($header, 6)))) {
323 1
            return [null, null];
324
        }
325
326 3
        if (strpos($decoded, ':') === false) {
327 1
            return [null, null]; // HTTP Basic header without colon isn't valid
328
        }
329
330 2
        return explode(':', $decoded, 2);
331
    }
332
333
    /**
334
     * Retrieve query string parameter.
335
     *
336
     * @param string                 $parameter
337
     * @param ServerRequestInterface $request
338
     * @param mixed                  $default
339
     *
340
     * @return null|string
341
     */
342 21
    protected function getQueryStringParameter($parameter, ServerRequestInterface $request, $default = null)
343
    {
344 21
        return isset($request->getQueryParams()[$parameter]) ? $request->getQueryParams()[$parameter] : $default;
345
    }
346
347
    /**
348
     * Retrieve cookie parameter.
349
     *
350
     * @param string                 $parameter
351
     * @param ServerRequestInterface $request
352
     * @param mixed                  $default
353
     *
354
     * @return null|string
355
     */
356 1
    protected function getCookieParameter($parameter, ServerRequestInterface $request, $default = null)
357
    {
358 1
        return isset($request->getCookieParams()[$parameter]) ? $request->getCookieParams()[$parameter] : $default;
359
    }
360
361
    /**
362
     * Retrieve server parameter.
363
     *
364
     * @param string                 $parameter
365
     * @param ServerRequestInterface $request
366
     * @param mixed                  $default
367
     *
368
     * @return null|string
369
     */
370 20
    protected function getServerParameter($parameter, ServerRequestInterface $request, $default = null)
371
    {
372 20
        return isset($request->getServerParams()[$parameter]) ? $request->getServerParams()[$parameter] : $default;
373
    }
374
375
    /**
376
     * Issue an access token.
377
     *
378
     * @param DateInterval           $accessTokenTTL
379
     * @param ClientEntityInterface  $client
380
     * @param string|null            $userIdentifier
381
     * @param ScopeEntityInterface[] $scopes
382
     *
383
     * @throws OAuthServerException
384
     * @throws UniqueTokenIdentifierConstraintViolationException
385
     *
386
     * @return AccessTokenEntityInterface
387
     */
388 20
    protected function issueAccessToken(
389
        DateInterval $accessTokenTTL,
390
        ClientEntityInterface $client,
391
        $userIdentifier,
392
        array $scopes = []
393
    ) {
394 20
        $maxGenerationAttempts = self::MAX_RANDOM_TOKEN_GENERATION_ATTEMPTS;
395
396 20
        $accessToken = $this->accessTokenRepository->getNewToken($client, $scopes, $userIdentifier);
397 20
        $accessToken->setClient($client);
398 20
        $accessToken->setUserIdentifier($userIdentifier);
399 20
        $accessToken->setExpiryDateTime((new DateTime())->add($accessTokenTTL));
400
401 20
        foreach ($scopes as $scope) {
402 16
            $accessToken->addScope($scope);
403
        }
404
405 20
        while ($maxGenerationAttempts-- > 0) {
406 20
            $accessToken->setIdentifier($this->generateUniqueIdentifier());
407
            try {
408 20
                $this->accessTokenRepository->persistNewAccessToken($accessToken);
409
410 18
                return $accessToken;
411 2
            } catch (UniqueTokenIdentifierConstraintViolationException $e) {
412 1
                if ($maxGenerationAttempts === 0) {
413 1
                    throw $e;
414
                }
415
            }
416
        }
417
    }
418
419
    /**
420
     * Issue an auth code.
421
     *
422
     * @param DateInterval           $authCodeTTL
423
     * @param ClientEntityInterface  $client
424
     * @param string                 $userIdentifier
425
     * @param string|null            $redirectUri
426
     * @param ScopeEntityInterface[] $scopes
427
     *
428
     * @throws OAuthServerException
429
     * @throws UniqueTokenIdentifierConstraintViolationException
430
     *
431
     * @return AuthCodeEntityInterface
432
     */
433 6
    protected function issueAuthCode(
434
        DateInterval $authCodeTTL,
435
        ClientEntityInterface $client,
436
        $userIdentifier,
437
        $redirectUri,
438
        array $scopes = []
439
    ) {
440 6
        $maxGenerationAttempts = self::MAX_RANDOM_TOKEN_GENERATION_ATTEMPTS;
441
442 6
        $authCode = $this->authCodeRepository->getNewAuthCode();
443 6
        $authCode->setExpiryDateTime((new DateTime())->add($authCodeTTL));
444 6
        $authCode->setClient($client);
445 6
        $authCode->setUserIdentifier($userIdentifier);
446
447 6
        if ($redirectUri !== null) {
448 1
            $authCode->setRedirectUri($redirectUri);
449
        }
450
451 6
        foreach ($scopes as $scope) {
452 1
            $authCode->addScope($scope);
453
        }
454
455 6
        while ($maxGenerationAttempts-- > 0) {
456 6
            $authCode->setIdentifier($this->generateUniqueIdentifier());
457
            try {
458 6
                $this->authCodeRepository->persistNewAuthCode($authCode);
459
460 4
                return $authCode;
461 2
            } catch (UniqueTokenIdentifierConstraintViolationException $e) {
462 1
                if ($maxGenerationAttempts === 0) {
463 1
                    throw $e;
464
                }
465
            }
466
        }
467
    }
468
469
    /**
470
     * @param AccessTokenEntityInterface $accessToken
471
     *
472
     * @throws OAuthServerException
473
     * @throws UniqueTokenIdentifierConstraintViolationException
474
     *
475
     * @return RefreshTokenEntityInterface|null
476
     */
477 14
    protected function issueRefreshToken(AccessTokenEntityInterface $accessToken)
478
    {
479 14
        $refreshToken = $this->refreshTokenRepository->getNewRefreshToken();
480
481 14
        if ($refreshToken === null) {
482 4
            return null;
483
        }
484
485 10
        $refreshToken->setExpiryDateTime((new DateTime())->add($this->refreshTokenTTL));
486 10
        $refreshToken->setAccessToken($accessToken);
487
488 10
        $maxGenerationAttempts = self::MAX_RANDOM_TOKEN_GENERATION_ATTEMPTS;
489
490 10
        while ($maxGenerationAttempts-- > 0) {
491 10
            $refreshToken->setIdentifier($this->generateUniqueIdentifier());
492
            try {
493 10
                $this->refreshTokenRepository->persistNewRefreshToken($refreshToken);
494
495 8
                return $refreshToken;
496 2
            } catch (UniqueTokenIdentifierConstraintViolationException $e) {
497 1
                if ($maxGenerationAttempts === 0) {
498 1
                    throw $e;
499
                }
500
            }
501
        }
502
    }
503
504
    /**
505
     * Generate a new unique identifier.
506
     *
507
     * @param int $length
508
     *
509
     * @throws OAuthServerException
510
     *
511
     * @return string
512
     */
513 28
    protected function generateUniqueIdentifier($length = 40)
514
    {
515
        try {
516 28
            return bin2hex(random_bytes($length));
517
            // @codeCoverageIgnoreStart
518
        } catch (TypeError $e) {
519
            throw OAuthServerException::serverError('An unexpected error has occurred', $e);
520
        } catch (Error $e) {
521
            throw OAuthServerException::serverError('An unexpected error has occurred', $e);
522
        } catch (Exception $e) {
523
            // If you get this message, the CSPRNG failed hard.
524
            throw OAuthServerException::serverError('Could not generate a random string', $e);
525
        }
526
        // @codeCoverageIgnoreEnd
527
    }
528
529
    /**
530
     * {@inheritdoc}
531
     */
532 5
    public function canRespondToAccessTokenRequest(ServerRequestInterface $request)
533
    {
534 5
        $requestParameters = (array) $request->getParsedBody();
535
536
        return (
537 5
            array_key_exists('grant_type', $requestParameters)
538 5
            && $requestParameters['grant_type'] === $this->getIdentifier()
539
        );
540
    }
541
542
    /**
543
     * {@inheritdoc}
544
     */
545 1
    public function canRespondToAuthorizationRequest(ServerRequestInterface $request)
546
    {
547 1
        return false;
548
    }
549
550
    /**
551
     * {@inheritdoc}
552
     */
553 1
    public function validateAuthorizationRequest(ServerRequestInterface $request)
554
    {
555 1
        throw new LogicException('This grant cannot validate an authorization request');
556
    }
557
558
    /**
559
     * {@inheritdoc}
560
     */
561 1
    public function completeAuthorizationRequest(AuthorizationRequest $authorizationRequest)
562
    {
563 1
        throw new LogicException('This grant cannot complete an authorization request');
564
    }
565
}
566