Passed
Push — master ( b1ca46...93ace5 )
by Andrew
29:49 queued 28:17
created

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