Completed
Pull Request — master (#887)
by
unknown
06:22 queued 35s
created

AbstractGrant::validateScopes()   B

Complexity

Conditions 5
Paths 9

Size

Total Lines 27
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 14
CRAP Score 5

Importance

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