Completed
Pull Request — master (#1035)
by Matt
03:17
created

AbstractGrant::validateRedirectUri()   A

Complexity

Conditions 5
Paths 3

Size

Total Lines 17

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 5

Importance

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