Passed
Pull Request — master (#1074)
by Andrew
02:07
created

AbstractGrant::setRefreshTokenRepository()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

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