Passed
Pull Request — master (#1189)
by
unknown
30:49
created

AbstractGrant::setRevokeRefreshTokens()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 1

Importance

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