Completed
Push — master ( 0bdd02...a34f5d )
by Andrew
24s
created

AbstractGrant::convertScopesQueryStringToArray()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

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