Completed
Push — master ( 28e141...99e42f )
by Andrew
06:01
created

AbstractGrant   D

Complexity

Total Complexity 56

Size/Duplication

Total Lines 486
Duplicated Lines 0 %

Coupling/Cohesion

Components 3
Dependencies 16

Test Coverage

Coverage 97.86%

Importance

Changes 0
Metric Value
wmc 56
c 0
b 0
f 0
lcom 3
cbo 16
dl 0
loc 486
ccs 137
cts 140
cp 0.9786
rs 4.5205

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