Passed
Push — master ( 8c33b5...d7634f )
by Andrew
05:35
created

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