Passed
Push — master ( d6e0b3...82e7b7 )
by Andrew
07:05 queued 11s
created

AbstractGrant::getQueryStringParameter()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 2

Importance

Changes 0
Metric Value
cc 2
eloc 1
c 0
b 0
f 0
nc 2
nop 3
dl 0
loc 3
ccs 2
cts 2
cp 1
crap 2
rs 10
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 DateTimeImmutable;
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
     * @var string
92
     */
93
    protected $defaultScope;
94
95
    /**
96
     * @param ClientRepositoryInterface $clientRepository
97
     */
98 81
    public function setClientRepository(ClientRepositoryInterface $clientRepository)
99
    {
100 81
        $this->clientRepository = $clientRepository;
101 81
    }
102
103
    /**
104
     * @param AccessTokenRepositoryInterface $accessTokenRepository
105
     */
106 46
    public function setAccessTokenRepository(AccessTokenRepositoryInterface $accessTokenRepository)
107
    {
108 46
        $this->accessTokenRepository = $accessTokenRepository;
109 46
    }
110
111
    /**
112
     * @param ScopeRepositoryInterface $scopeRepository
113
     */
114 45
    public function setScopeRepository(ScopeRepositoryInterface $scopeRepository)
115
    {
116 45
        $this->scopeRepository = $scopeRepository;
117 45
    }
118
119
    /**
120
     * @param RefreshTokenRepositoryInterface $refreshTokenRepository
121
     */
122 67
    public function setRefreshTokenRepository(RefreshTokenRepositoryInterface $refreshTokenRepository)
123
    {
124 67
        $this->refreshTokenRepository = $refreshTokenRepository;
125 67
    }
126
127
    /**
128
     * @param AuthCodeRepositoryInterface $authCodeRepository
129
     */
130 50
    public function setAuthCodeRepository(AuthCodeRepositoryInterface $authCodeRepository)
131
    {
132 50
        $this->authCodeRepository = $authCodeRepository;
133 50
    }
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 35
    public function setPrivateKey(CryptKey $key)
157
    {
158 35
        $this->privateKey = $key;
159 35
    }
160
161
    /**
162
     * @param string $scope
163
     */
164 20
    public function setDefaultScope($scope)
165
    {
166 20
        $this->defaultScope = $scope;
167 20
    }
168
169
    /**
170
     * Validate the client.
171
     *
172
     * @param ServerRequestInterface $request
173
     *
174
     * @throws OAuthServerException
175
     *
176
     * @return ClientEntityInterface
177
     */
178 44
    protected function validateClient(ServerRequestInterface $request)
179
    {
180 44
        list($clientId, $clientSecret) = $this->getClientCredentials($request);
181
182 42
        if ($this->clientRepository->validateClient($clientId, $clientSecret, $this->getIdentifier()) === false) {
183 7
            $this->getEmitter()->emit(new RequestEvent(RequestEvent::CLIENT_AUTHENTICATION_FAILED, $request));
184
185 7
            throw OAuthServerException::invalidClient($request);
186
        }
187
188 35
        $client = $this->getClientEntityOrFail($clientId, $request);
189
190
        // If a redirect URI is provided ensure it matches what is pre-registered
191 35
        $redirectUri = $this->getRequestParameter('redirect_uri', $request, null);
192
193 35
        if ($redirectUri !== null) {
194 17
            if (!\is_string($redirectUri)) {
0 ignored issues
show
introduced by
The condition is_string($redirectUri) is always true.
Loading history...
195 1
                throw OAuthServerException::invalidRequest('redirect_uri');
196
            }
197
198 16
            $this->validateRedirectUri($redirectUri, $client, $request);
199
        }
200
201 32
        return $client;
202
    }
203
204
    /**
205
     * Wrapper around ClientRepository::getClientEntity() that ensures we emit
206
     * an event and throw an exception if the repo doesn't return a client
207
     * entity.
208
     *
209
     * This is a bit of defensive coding because the interface contract
210
     * doesn't actually enforce non-null returns/exception-on-no-client so
211
     * getClientEntity might return null. By contrast, this method will
212
     * always either return a ClientEntityInterface or throw.
213
     *
214
     * @param string                 $clientId
215
     * @param ServerRequestInterface $request
216
     *
217
     * @return ClientEntityInterface
218
     */
219 66
    protected function getClientEntityOrFail($clientId, ServerRequestInterface $request)
220
    {
221 66
        $client = $this->clientRepository->getClientEntity($clientId);
222
223 66
        if ($client instanceof ClientEntityInterface === false) {
224 4
            $this->getEmitter()->emit(new RequestEvent(RequestEvent::CLIENT_AUTHENTICATION_FAILED, $request));
225 4
            throw OAuthServerException::invalidClient($request);
226
        }
227
228 62
        return $client;
229
    }
230
231
    /**
232
     * Gets the client credentials from the request from the request body or
233
     * the Http Basic Authorization header
234
     *
235
     * @param ServerRequestInterface $request
236
     *
237
     * @return array
238
     */
239 56
    protected function getClientCredentials(ServerRequestInterface $request)
240
    {
241 56
        list($basicAuthUser, $basicAuthPassword) = $this->getBasicAuthCredentials($request);
242
243 56
        $clientId = $this->getRequestParameter('client_id', $request, $basicAuthUser);
244
245 56
        if (!\is_string($clientId)) {
246 3
            throw OAuthServerException::invalidRequest('client_id');
247
        }
248
249 53
        $clientSecret = $this->getRequestParameter('client_secret', $request, $basicAuthPassword);
250
251 53
        if ($clientSecret !== null && !\is_string($clientSecret)) {
0 ignored issues
show
introduced by
The condition is_string($clientSecret) is always true.
Loading history...
252 1
            throw OAuthServerException::invalidRequest('client_secret');
253
        }
254
255 52
        return [$clientId, $clientSecret];
256
    }
257
258
    /**
259
     * Validate redirectUri from the request.
260
     * If a redirect URI is provided ensure it matches what is pre-registered
261
     *
262
     * @param string                 $redirectUri
263
     * @param ClientEntityInterface  $client
264
     * @param ServerRequestInterface $request
265
     *
266
     * @throws OAuthServerException
267
     */
268 31
    protected function validateRedirectUri(
269
        string $redirectUri,
270
        ClientEntityInterface $client,
271
        ServerRequestInterface $request
272
    ) {
273 31
        if (\is_string($client->getRedirectUri())
274 31
            && (\strcmp($client->getRedirectUri(), $redirectUri) !== 0)
275
        ) {
276 3
            $this->getEmitter()->emit(new RequestEvent(RequestEvent::CLIENT_AUTHENTICATION_FAILED, $request));
277 3
            throw OAuthServerException::invalidClient($request);
278 28
        } elseif (\is_array($client->getRedirectUri())
279 28
            && \in_array($redirectUri, $client->getRedirectUri(), true) === false
280
        ) {
281 3
            $this->getEmitter()->emit(new RequestEvent(RequestEvent::CLIENT_AUTHENTICATION_FAILED, $request));
282 3
            throw OAuthServerException::invalidClient($request);
283
        }
284 25
    }
285
286
    /**
287
     * Validate scopes in the request.
288
     *
289
     * @param string|array $scopes
290
     * @param string       $redirectUri
291
     *
292
     * @throws OAuthServerException
293
     *
294
     * @return ScopeEntityInterface[]
295
     */
296 40
    public function validateScopes($scopes, $redirectUri = null)
297
    {
298 40
        if ($scopes === null) {
0 ignored issues
show
introduced by
The condition $scopes === null is always false.
Loading history...
299 5
            $scopes = [];
300 35
        } elseif (\is_string($scopes)) {
301 21
            $scopes = $this->convertScopesQueryStringToArray($scopes);
302
        }
303
304 40
        if (!\is_array($scopes)) {
0 ignored issues
show
introduced by
The condition is_array($scopes) is always true.
Loading history...
305
            throw OAuthServerException::invalidRequest('scope');
306
        }
307
308 40
        $validScopes = [];
309
310 40
        foreach ($scopes as $scopeItem) {
311 35
            $scope = $this->scopeRepository->getScopeEntityByIdentifier($scopeItem);
312
313 35
            if ($scope instanceof ScopeEntityInterface === false) {
314 2
                throw OAuthServerException::invalidScope($scopeItem, $redirectUri);
315
            }
316
317 33
            $validScopes[] = $scope;
318
        }
319
320 38
        return $validScopes;
321
    }
322
323
    /**
324
     * Converts a scopes query string to an array to easily iterate for validation.
325
     *
326
     * @param string $scopes
327
     *
328
     * @return array
329
     */
330 21
    private function convertScopesQueryStringToArray(string $scopes)
331
    {
332
        return \array_filter(\explode(self::SCOPE_DELIMITER_STRING, \trim($scopes)), function ($scope) {
333 21
            return !empty($scope);
334 21
        });
335
    }
336
337
    /**
338
     * Retrieve request parameter.
339
     *
340
     * @param string                 $parameter
341
     * @param ServerRequestInterface $request
342
     * @param mixed                  $default
343
     *
344
     * @return null|string
345
     */
346 56
    protected function getRequestParameter($parameter, ServerRequestInterface $request, $default = null)
347
    {
348 56
        $requestParameters = (array) $request->getParsedBody();
349
350 56
        return $requestParameters[$parameter] ?? $default;
351
    }
352
353
    /**
354
     * Retrieve HTTP Basic Auth credentials with the Authorization header
355
     * of a request. First index of the returned array is the username,
356
     * second is the password (so list() will work). If the header does
357
     * not exist, or is otherwise an invalid HTTP Basic header, return
358
     * [null, null].
359
     *
360
     * @param ServerRequestInterface $request
361
     *
362
     * @return string[]|null[]
363
     */
364 61
    protected function getBasicAuthCredentials(ServerRequestInterface $request)
365
    {
366 61
        if (!$request->hasHeader('Authorization')) {
367 52
            return [null, null];
368
        }
369
370 9
        $header = $request->getHeader('Authorization')[0];
371 9
        if (\strpos($header, 'Basic ') !== 0) {
372 3
            return [null, null];
373
        }
374
375 6
        if (!($decoded = \base64_decode(\substr($header, 6)))) {
376 1
            return [null, null];
377
        }
378
379 5
        if (\strpos($decoded, ':') === false) {
380 2
            return [null, null]; // HTTP Basic header without colon isn't valid
381
        }
382
383 3
        return \explode(':', $decoded, 2);
384
    }
385
386
    /**
387
     * Retrieve query string parameter.
388
     *
389
     * @param string                 $parameter
390
     * @param ServerRequestInterface $request
391
     * @param mixed                  $default
392
     *
393
     * @return null|string
394
     */
395 24
    protected function getQueryStringParameter($parameter, ServerRequestInterface $request, $default = null)
396
    {
397 24
        return isset($request->getQueryParams()[$parameter]) ? $request->getQueryParams()[$parameter] : $default;
398
    }
399
400
    /**
401
     * Retrieve cookie parameter.
402
     *
403
     * @param string                 $parameter
404
     * @param ServerRequestInterface $request
405
     * @param mixed                  $default
406
     *
407
     * @return null|string
408
     */
409 1
    protected function getCookieParameter($parameter, ServerRequestInterface $request, $default = null)
410
    {
411 1
        return isset($request->getCookieParams()[$parameter]) ? $request->getCookieParams()[$parameter] : $default;
412
    }
413
414
    /**
415
     * Retrieve server parameter.
416
     *
417
     * @param string                 $parameter
418
     * @param ServerRequestInterface $request
419
     * @param mixed                  $default
420
     *
421
     * @return null|string
422
     */
423 23
    protected function getServerParameter($parameter, ServerRequestInterface $request, $default = null)
424
    {
425 23
        return isset($request->getServerParams()[$parameter]) ? $request->getServerParams()[$parameter] : $default;
426
    }
427
428
    /**
429
     * Issue an access token.
430
     *
431
     * @param DateInterval           $accessTokenTTL
432
     * @param ClientEntityInterface  $client
433
     * @param string|null            $userIdentifier
434
     * @param ScopeEntityInterface[] $scopes
435
     *
436
     * @throws OAuthServerException
437
     * @throws UniqueTokenIdentifierConstraintViolationException
438
     *
439
     * @return AccessTokenEntityInterface
440
     */
441 22
    protected function issueAccessToken(
442
        DateInterval $accessTokenTTL,
443
        ClientEntityInterface $client,
444
        $userIdentifier,
445
        array $scopes = []
446
    ) {
447 22
        $maxGenerationAttempts = self::MAX_RANDOM_TOKEN_GENERATION_ATTEMPTS;
448
449 22
        $accessToken = $this->accessTokenRepository->getNewToken($client, $scopes, $userIdentifier);
450 22
        $accessToken->setExpiryDateTime((new DateTimeImmutable())->add($accessTokenTTL));
451 22
        $accessToken->setPrivateKey($this->privateKey);
452
453 22
        while ($maxGenerationAttempts-- > 0) {
454 22
            $accessToken->setIdentifier($this->generateUniqueIdentifier());
455
            try {
456 22
                $this->accessTokenRepository->persistNewAccessToken($accessToken);
457
458 20
                return $accessToken;
459 3
            } catch (UniqueTokenIdentifierConstraintViolationException $e) {
460 2
                if ($maxGenerationAttempts === 0) {
461 1
                    throw $e;
462
                }
463
            }
464
        }
465
    }
466
467
    /**
468
     * Issue an auth code.
469
     *
470
     * @param DateInterval           $authCodeTTL
471
     * @param ClientEntityInterface  $client
472
     * @param string                 $userIdentifier
473
     * @param string|null            $redirectUri
474
     * @param ScopeEntityInterface[] $scopes
475
     *
476
     * @throws OAuthServerException
477
     * @throws UniqueTokenIdentifierConstraintViolationException
478
     *
479
     * @return AuthCodeEntityInterface
480
     */
481 6
    protected function issueAuthCode(
482
        DateInterval $authCodeTTL,
483
        ClientEntityInterface $client,
484
        $userIdentifier,
485
        $redirectUri,
486
        array $scopes = []
487
    ) {
488 6
        $maxGenerationAttempts = self::MAX_RANDOM_TOKEN_GENERATION_ATTEMPTS;
489
490 6
        $authCode = $this->authCodeRepository->getNewAuthCode();
491 6
        $authCode->setExpiryDateTime((new DateTimeImmutable())->add($authCodeTTL));
492 6
        $authCode->setClient($client);
493 6
        $authCode->setUserIdentifier($userIdentifier);
494
495 6
        if ($redirectUri !== null) {
496 1
            $authCode->setRedirectUri($redirectUri);
497
        }
498
499 6
        foreach ($scopes as $scope) {
500 1
            $authCode->addScope($scope);
501
        }
502
503 6
        while ($maxGenerationAttempts-- > 0) {
504 6
            $authCode->setIdentifier($this->generateUniqueIdentifier());
505
            try {
506 6
                $this->authCodeRepository->persistNewAuthCode($authCode);
507
508 4
                return $authCode;
509 3
            } catch (UniqueTokenIdentifierConstraintViolationException $e) {
510 2
                if ($maxGenerationAttempts === 0) {
511 1
                    throw $e;
512
                }
513
            }
514
        }
515
    }
516
517
    /**
518
     * @param AccessTokenEntityInterface $accessToken
519
     *
520
     * @throws OAuthServerException
521
     * @throws UniqueTokenIdentifierConstraintViolationException
522
     *
523
     * @return RefreshTokenEntityInterface|null
524
     */
525 16
    protected function issueRefreshToken(AccessTokenEntityInterface $accessToken)
526
    {
527 16
        $refreshToken = $this->refreshTokenRepository->getNewRefreshToken();
528
529 16
        if ($refreshToken === null) {
530 4
            return null;
531
        }
532
533 12
        $refreshToken->setExpiryDateTime((new DateTimeImmutable())->add($this->refreshTokenTTL));
534 12
        $refreshToken->setAccessToken($accessToken);
535
536 12
        $maxGenerationAttempts = self::MAX_RANDOM_TOKEN_GENERATION_ATTEMPTS;
537
538 12
        while ($maxGenerationAttempts-- > 0) {
539 12
            $refreshToken->setIdentifier($this->generateUniqueIdentifier());
540
            try {
541 12
                $this->refreshTokenRepository->persistNewRefreshToken($refreshToken);
542
543 10
                return $refreshToken;
544 3
            } catch (UniqueTokenIdentifierConstraintViolationException $e) {
545 2
                if ($maxGenerationAttempts === 0) {
546 1
                    throw $e;
547
                }
548
            }
549
        }
550
    }
551
552
    /**
553
     * Generate a new unique identifier.
554
     *
555
     * @param int $length
556
     *
557
     * @throws OAuthServerException
558
     *
559
     * @return string
560
     */
561 30
    protected function generateUniqueIdentifier($length = 40)
562
    {
563
        try {
564 30
            return \bin2hex(\random_bytes($length));
565
            // @codeCoverageIgnoreStart
566
        } catch (TypeError $e) {
567
            throw OAuthServerException::serverError('An unexpected error has occurred', $e);
568
        } catch (Error $e) {
569
            throw OAuthServerException::serverError('An unexpected error has occurred', $e);
570
        } catch (Exception $e) {
571
            // If you get this message, the CSPRNG failed hard.
572
            throw OAuthServerException::serverError('Could not generate a random string', $e);
573
        }
574
        // @codeCoverageIgnoreEnd
575
    }
576
577
    /**
578
     * {@inheritdoc}
579
     */
580 5
    public function canRespondToAccessTokenRequest(ServerRequestInterface $request)
581
    {
582 5
        $requestParameters = (array) $request->getParsedBody();
583
584
        return (
585 5
            \array_key_exists('grant_type', $requestParameters)
586 5
            && $requestParameters['grant_type'] === $this->getIdentifier()
587
        );
588
    }
589
590
    /**
591
     * {@inheritdoc}
592
     */
593 1
    public function canRespondToAuthorizationRequest(ServerRequestInterface $request)
594
    {
595 1
        return false;
596
    }
597
598
    /**
599
     * {@inheritdoc}
600
     */
601 1
    public function validateAuthorizationRequest(ServerRequestInterface $request)
602
    {
603 1
        throw new LogicException('This grant cannot validate an authorization request');
604
    }
605
606
    /**
607
     * {@inheritdoc}
608
     */
609 1
    public function completeAuthorizationRequest(AuthorizationRequest $authorizationRequest)
610
    {
611 1
        throw new LogicException('This grant cannot complete an authorization request');
612
    }
613
}
614