Completed
Pull Request — master (#887)
by
unknown
02:49
created

AbstractGrant::validateAuthorizationRequest()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 4
ccs 2
cts 2
cp 1
rs 10
cc 1
eloc 2
nc 1
nop 1
crap 1
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 1
        return $scopesList;
272
    }
273
274
    /**
275
     * Retrieve request parameter.
276
     *
277
     * @param string                 $parameter
278
     * @param ServerRequestInterface $request
279
     * @param mixed                  $default
280
     *
281
     * @return null|string
282
     */
283 42
    protected function getRequestParameter($parameter, ServerRequestInterface $request, $default = null)
284
    {
285 42
        $requestParameters = (array) $request->getParsedBody();
286
287 42
        return isset($requestParameters[$parameter]) ? $requestParameters[$parameter] : $default;
288
    }
289
290
    /**
291
     * Retrieve HTTP Basic Auth credentials with the Authorization header
292
     * of a request. First index of the returned array is the username,
293
     * second is the password (so list() will work). If the header does
294
     * not exist, or is otherwise an invalid HTTP Basic header, return
295
     * [null, null].
296
     *
297
     * @param ServerRequestInterface $request
298
     *
299
     * @return string[]|null[]
300
     */
301 47
    protected function getBasicAuthCredentials(ServerRequestInterface $request)
302
    {
303 47
        if (!$request->hasHeader('Authorization')) {
304 42
            return [null, null];
305
        }
306
307 5
        $header = $request->getHeader('Authorization')[0];
308 5
        if (strpos($header, 'Basic ') !== 0) {
309 1
            return [null, null];
310
        }
311
312 4
        if (!($decoded = base64_decode(substr($header, 6)))) {
313 1
            return [null, null];
314
        }
315
316 3
        if (strpos($decoded, ':') === false) {
317 1
            return [null, null]; // HTTP Basic header without colon isn't valid
318
        }
319
320 2
        return explode(':', $decoded, 2);
321
    }
322
323
    /**
324
     * Retrieve query string parameter.
325
     *
326
     * @param string                 $parameter
327
     * @param ServerRequestInterface $request
328
     * @param mixed                  $default
329
     *
330
     * @return null|string
331
     */
332 22
    protected function getQueryStringParameter($parameter, ServerRequestInterface $request, $default = null)
333
    {
334 22
        return isset($request->getQueryParams()[$parameter]) ? $request->getQueryParams()[$parameter] : $default;
335
    }
336
337
    /**
338
     * Retrieve cookie parameter.
339
     *
340
     * @param string                 $parameter
341
     * @param ServerRequestInterface $request
342
     * @param mixed                  $default
343
     *
344
     * @return null|string
345
     */
346 1
    protected function getCookieParameter($parameter, ServerRequestInterface $request, $default = null)
347
    {
348 1
        return isset($request->getCookieParams()[$parameter]) ? $request->getCookieParams()[$parameter] : $default;
349
    }
350
351
    /**
352
     * Retrieve server parameter.
353
     *
354
     * @param string                 $parameter
355
     * @param ServerRequestInterface $request
356
     * @param mixed                  $default
357
     *
358
     * @return null|string
359
     */
360 21
    protected function getServerParameter($parameter, ServerRequestInterface $request, $default = null)
361
    {
362 21
        return isset($request->getServerParams()[$parameter]) ? $request->getServerParams()[$parameter] : $default;
363
    }
364
365
    /**
366
     * Issue an access token.
367
     *
368
     * @param \DateInterval          $accessTokenTTL
369
     * @param ClientEntityInterface  $client
370
     * @param string|null            $userIdentifier
371
     * @param ScopeEntityInterface[] $scopes
372
     *
373
     * @throws OAuthServerException
374
     * @throws UniqueTokenIdentifierConstraintViolationException
375
     *
376
     * @return AccessTokenEntityInterface
377
     */
378 17
    protected function issueAccessToken(
379
        \DateInterval $accessTokenTTL,
380
        ClientEntityInterface $client,
381
        $userIdentifier,
382
        array $scopes = []
383
    ) {
384 17
        $maxGenerationAttempts = self::MAX_RANDOM_TOKEN_GENERATION_ATTEMPTS;
385
386 17
        $accessToken = $this->accessTokenRepository->getNewToken($client, $scopes, $userIdentifier);
387 17
        $accessToken->setClient($client);
388 17
        $accessToken->setUserIdentifier($userIdentifier);
389 17
        $accessToken->setExpiryDateTime((new \DateTime())->add($accessTokenTTL));
390
391 17
        foreach ($scopes as $scope) {
392 13
            $accessToken->addScope($scope);
393
        }
394
395 17
        while ($maxGenerationAttempts-- > 0) {
396 17
            $accessToken->setIdentifier($this->generateUniqueIdentifier());
397
            try {
398 17
                $this->accessTokenRepository->persistNewAccessToken($accessToken);
399
400 15
                return $accessToken;
401 2
            } catch (UniqueTokenIdentifierConstraintViolationException $e) {
402 1
                if ($maxGenerationAttempts === 0) {
403 1
                    throw $e;
404
                }
405
            }
406
        }
407
    }
408
409
    /**
410
     * Issue an auth code.
411
     *
412
     * @param \DateInterval          $authCodeTTL
413
     * @param ClientEntityInterface  $client
414
     * @param string                 $userIdentifier
415
     * @param string                 $redirectUri
416
     * @param ScopeEntityInterface[] $scopes
417
     *
418
     * @throws OAuthServerException
419
     * @throws UniqueTokenIdentifierConstraintViolationException
420
     *
421
     * @return AuthCodeEntityInterface
422
     */
423 6
    protected function issueAuthCode(
424
        \DateInterval $authCodeTTL,
425
        ClientEntityInterface $client,
426
        $userIdentifier,
427
        $redirectUri,
428
        array $scopes = []
429
    ) {
430 6
        $maxGenerationAttempts = self::MAX_RANDOM_TOKEN_GENERATION_ATTEMPTS;
431
432 6
        $authCode = $this->authCodeRepository->getNewAuthCode();
433 6
        $authCode->setExpiryDateTime((new \DateTime())->add($authCodeTTL));
434 6
        $authCode->setClient($client);
435 6
        $authCode->setUserIdentifier($userIdentifier);
436 6
        $authCode->setRedirectUri($redirectUri);
437
438 6
        foreach ($scopes as $scope) {
439 1
            $authCode->addScope($scope);
440
        }
441
442 6
        while ($maxGenerationAttempts-- > 0) {
443 6
            $authCode->setIdentifier($this->generateUniqueIdentifier());
444
            try {
445 6
                $this->authCodeRepository->persistNewAuthCode($authCode);
446
447 4
                return $authCode;
448 2
            } catch (UniqueTokenIdentifierConstraintViolationException $e) {
449 1
                if ($maxGenerationAttempts === 0) {
450 1
                    throw $e;
451
                }
452
            }
453
        }
454
    }
455
456
    /**
457
     * @param AccessTokenEntityInterface $accessToken
458
     *
459
     * @throws OAuthServerException
460
     * @throws UniqueTokenIdentifierConstraintViolationException
461
     *
462
     * @return RefreshTokenEntityInterface
463
     */
464 10
    protected function issueRefreshToken(AccessTokenEntityInterface $accessToken)
465
    {
466 10
        $maxGenerationAttempts = self::MAX_RANDOM_TOKEN_GENERATION_ATTEMPTS;
467
468 10
        $refreshToken = $this->refreshTokenRepository->getNewRefreshToken();
469 10
        $refreshToken->setExpiryDateTime((new \DateTime())->add($this->refreshTokenTTL));
470 10
        $refreshToken->setAccessToken($accessToken);
471
472 10
        while ($maxGenerationAttempts-- > 0) {
473 10
            $refreshToken->setIdentifier($this->generateUniqueIdentifier());
474
            try {
475 10
                $this->refreshTokenRepository->persistNewRefreshToken($refreshToken);
476
477 8
                return $refreshToken;
478 2
            } catch (UniqueTokenIdentifierConstraintViolationException $e) {
479 1
                if ($maxGenerationAttempts === 0) {
480 1
                    throw $e;
481
                }
482
            }
483
        }
484
    }
485
486
    /**
487
     * Generate a new unique identifier.
488
     *
489
     * @param int $length
490
     *
491
     * @throws OAuthServerException
492
     *
493
     * @return string
494
     */
495 25
    protected function generateUniqueIdentifier($length = 40)
496
    {
497
        try {
498 25
            return bin2hex(random_bytes($length));
499
            // @codeCoverageIgnoreStart
500
        } catch (\TypeError $e) {
501
            throw OAuthServerException::serverError('An unexpected error has occurred');
502
        } catch (\Error $e) {
503
            throw OAuthServerException::serverError('An unexpected error has occurred');
504
        } catch (\Exception $e) {
505
            // If you get this message, the CSPRNG failed hard.
506
            throw OAuthServerException::serverError('Could not generate a random string');
507
        }
508
        // @codeCoverageIgnoreEnd
509
    }
510
511
    /**
512
     * {@inheritdoc}
513
     */
514 5
    public function canRespondToAccessTokenRequest(ServerRequestInterface $request)
515
    {
516 5
        $requestParameters = (array) $request->getParsedBody();
517
518
        return (
519 5
            array_key_exists('grant_type', $requestParameters)
520 5
            && $requestParameters['grant_type'] === $this->getIdentifier()
521
        );
522
    }
523
524
    /**
525
     * {@inheritdoc}
526
     */
527 1
    public function canRespondToAuthorizationRequest(ServerRequestInterface $request)
528
    {
529 1
        return false;
530
    }
531
532
    /**
533
     * {@inheritdoc}
534
     */
535 1
    public function validateAuthorizationRequest(ServerRequestInterface $request)
536
    {
537 1
        throw new \LogicException('This grant cannot validate an authorization request');
538
    }
539
540
    /**
541
     * {@inheritdoc}
542
     */
543 1
    public function completeAuthorizationRequest(AuthorizationRequest $authorizationRequest)
544
    {
545 1
        throw new \LogicException('This grant cannot complete an authorization request');
546
    }
547
}
548