Completed
Pull Request — master (#788)
by
unknown
34:50
created

AbstractGrant::getQueryStringParameter()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 4
rs 10
c 0
b 0
f 0
cc 2
eloc 2
nc 2
nop 3
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
     * @param ClientRepositoryInterface $clientRepository
86
     */
87
    public function setClientRepository(ClientRepositoryInterface $clientRepository)
88
    {
89
        $this->clientRepository = $clientRepository;
90
    }
91
92
    /**
93
     * @param AccessTokenRepositoryInterface $accessTokenRepository
94
     */
95
    public function setAccessTokenRepository(AccessTokenRepositoryInterface $accessTokenRepository)
96
    {
97
        $this->accessTokenRepository = $accessTokenRepository;
98
    }
99
100
    /**
101
     * @param ScopeRepositoryInterface $scopeRepository
102
     */
103
    public function setScopeRepository(ScopeRepositoryInterface $scopeRepository)
104
    {
105
        $this->scopeRepository = $scopeRepository;
106
    }
107
108
    /**
109
     * @param RefreshTokenRepositoryInterface $refreshTokenRepository
110
     */
111
    public function setRefreshTokenRepository(RefreshTokenRepositoryInterface $refreshTokenRepository)
112
    {
113
        $this->refreshTokenRepository = $refreshTokenRepository;
114
    }
115
116
    /**
117
     * @param AuthCodeRepositoryInterface $authCodeRepository
118
     */
119
    public function setAuthCodeRepository(AuthCodeRepositoryInterface $authCodeRepository)
120
    {
121
        $this->authCodeRepository = $authCodeRepository;
122
    }
123
124
    /**
125
     * @param UserRepositoryInterface $userRepository
126
     */
127
    public function setUserRepository(UserRepositoryInterface $userRepository)
128
    {
129
        $this->userRepository = $userRepository;
130
    }
131
132
    /**
133
     * {@inheritdoc}
134
     */
135
    public function setRefreshTokenTTL(\DateInterval $refreshTokenTTL)
136
    {
137
        $this->refreshTokenTTL = $refreshTokenTTL;
138
    }
139
140
    /**
141
     * Set the private key
142
     *
143
     * @param \League\OAuth2\Server\CryptKey $key
144
     */
145
    public function setPrivateKey(CryptKey $key)
146
    {
147
        $this->privateKey = $key;
148
    }
149
150
    /**
151
     * Validate the client.
152
     *
153
     * @param ServerRequestInterface $request
154
     *
155
     * @throws OAuthServerException
156
     *
157
     * @return ClientEntityInterface
158
     */
159
    protected function validateClient(ServerRequestInterface $request)
160
    {
161
        list($basicAuthUser, $basicAuthPassword) = $this->getBasicAuthCredentials($request);
162
163
        $clientId = $this->getRequestParameter('client_id', $request, $basicAuthUser);
164
        if (is_null($clientId)) {
165
            throw OAuthServerException::invalidRequest('client_id');
166
        }
167
168
        // If the client is confidential require the client secret
169
        $clientSecret = $this->getRequestParameter('client_secret', $request, $basicAuthPassword);
170
171
        $client = $this->clientRepository->getClientEntity(
172
            $clientId,
173
            $this->getIdentifier(),
174
            $clientSecret,
175
            true
176
        );
177
178
        if ($client instanceof ClientEntityInterface === false) {
179
            $this->getEmitter()->emit(new RequestEvent(RequestEvent::CLIENT_AUTHENTICATION_FAILED, $request));
180
            throw OAuthServerException::invalidClient();
181
        }
182
183
        // If a redirect URI is provided ensure it matches what is pre-registered
184
        $redirectUri = $this->getRequestParameter('redirect_uri', $request, null);
185
        if ($redirectUri !== null) {
186
            if (is_string($client->getRedirectUri())) {
187
                if ((strcmp($client->getRedirectUri(), $redirectUri) !== 0)) {
188
                    $registered_domain = $this->getDomain($client->getRedirectUri());
189
                    $requested_domain = $this->getDomain($redirectUri, count(explode('.', $registered_domain)));
190
                    if ((!$registered_domain || !$requested_domain) || $registered_domain !== $requested_domain) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $registered_domain of type null|string is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
Bug Best Practice introduced by
The expression $requested_domain of type null|string is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
191
                        $this->getEmitter()->emit(new RequestEvent(RequestEvent::CLIENT_AUTHENTICATION_FAILED, $request));
192
                        throw OAuthServerException::invalidClient();
193
                    }
194
                }
195
196
            } elseif (
197
                is_array($client->getRedirectUri())
198
                && in_array($redirectUri, $client->getRedirectUri()) === false
199
            ) {
200
                $invalid_client = true;
201
                foreach ($client->getRedirectUri() as $url) {
202
                    $registered_domain = $this->getDomain($url);
203
                    $requested_domain = $this->getDomain($redirectUri, count(explode('.', $registered_domain)));
204
                    if (($registered_domain && $requested_domain) && $registered_domain == $requested_domain) {
205
                        $invalid_client = false;
206
                        break;
207
                    }
208
                }
209
                if ($invalid_client) {
210
                    $this->getEmitter()->emit(new RequestEvent(RequestEvent::CLIENT_AUTHENTICATION_FAILED, $request));
211
                    throw OAuthServerException::invalidClient();
212
                }
213
            }
214
        }
215
216
        return $client;
217
    }
218
219
    /**
220
     * Get domain from the URL
221
     *
222
     * @param $redirectUri
223
     * @param int $sub_domain_depth (2 - domain.com, 3 - subdomain.domain.com, etc.)
224
     * @return null|string
225
     */
226
    protected function getDomain($redirectUri, $sub_domain_depth = null)
227
    {
228
        try {
229
            $host = parse_url($redirectUri)['host'];
230
            if ($sub_domain_depth) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $sub_domain_depth of type integer|null is loosely compared to true; this is ambiguous if the integer can be zero. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
231
                $url_array = explode('.', $host);
232
                array_splice($url_array, 0, -$sub_domain_depth);
233
                $host = implode(".", $url_array);
234
            }
235
            return $host;
236
        } catch (\Exception $exception) {
237
            return null;
238
        }
239
    }
240
241
242
    /**
243
     * Validate scopes in the request.
244
     *
245
     * @param string $scopes
246
     * @param string $redirectUri
247
     *
248
     * @throws OAuthServerException
249
     *
250
     * @return ScopeEntityInterface[]
251
     */
252
    public function validateScopes(
253
        $scopes,
254
        $redirectUri = null
255
    )
256
    {
257
        $scopesList = array_filter(
258
            explode(self::SCOPE_DELIMITER_STRING, trim($scopes)),
259
            function ($scope) {
260
                return !empty($scope);
261
            }
262
        );
263
264
        $scopes = [];
265
        foreach ($scopesList as $scopeItem) {
266
            $scope = $this->scopeRepository->getScopeEntityByIdentifier($scopeItem);
267
268
            if ($scope instanceof ScopeEntityInterface === false) {
269
                throw OAuthServerException::invalidScope($scopeItem, $redirectUri);
270
            }
271
272
            $scopes[] = $scope;
273
        }
274
275
        return $scopes;
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
    protected function getRequestParameter($parameter, ServerRequestInterface $request, $default = null)
288
    {
289
        $requestParameters = (array)$request->getParsedBody();
290
291
        return isset($requestParameters[$parameter]) ? $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
    protected function getBasicAuthCredentials(ServerRequestInterface $request)
306
    {
307
        if (!$request->hasHeader('Authorization')) {
308
            return [null, null];
309
        }
310
311
        $header = $request->getHeader('Authorization')[0];
312
        if (strpos($header, 'Basic ') !== 0) {
313
            return [null, null];
314
        }
315
316
        if (!($decoded = base64_decode(substr($header, 6)))) {
317
            return [null, null];
318
        }
319
320
        if (strpos($decoded, ':') === false) {
321
            return [null, null]; // HTTP Basic header without colon isn't valid
322
        }
323
324
        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
    protected function getQueryStringParameter($parameter, ServerRequestInterface $request, $default = null)
337
    {
338
        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
    protected function getCookieParameter($parameter, ServerRequestInterface $request, $default = null)
351
    {
352
        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
    protected function getServerParameter($parameter, ServerRequestInterface $request, $default = null)
365
    {
366
        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 $userIdentifier
375
     * @param ScopeEntityInterface[] $scopes
376
     *
377
     * @throws OAuthServerException
378
     * @throws UniqueTokenIdentifierConstraintViolationException
379
     *
380
     * @return AccessTokenEntityInterface
381
     */
382
    protected function issueAccessToken(
383
        \DateInterval $accessTokenTTL,
384
        ClientEntityInterface $client,
385
        $userIdentifier,
386
        array $scopes = []
387
    )
388
    {
389
        $maxGenerationAttempts = self::MAX_RANDOM_TOKEN_GENERATION_ATTEMPTS;
390
391
        $accessToken = $this->accessTokenRepository->getNewToken($client, $scopes, $userIdentifier);
392
        $accessToken->setClient($client);
393
        $accessToken->setUserIdentifier($userIdentifier);
394
        $accessToken->setExpiryDateTime((new \DateTime())->add($accessTokenTTL));
395
396
        foreach ($scopes as $scope) {
397
            $accessToken->addScope($scope);
398
        }
399
400
        while ($maxGenerationAttempts-- > 0) {
401
            $accessToken->setIdentifier($this->generateUniqueIdentifier());
402
            try {
403
                $this->accessTokenRepository->persistNewAccessToken($accessToken);
404
405
                return $accessToken;
406
            } catch (UniqueTokenIdentifierConstraintViolationException $e) {
407
                if ($maxGenerationAttempts === 0) {
408
                    throw $e;
409
                }
410
            }
411
        }
412
    }
413
414
    /**
415
     * Issue an auth code.
416
     *
417
     * @param \DateInterval $authCodeTTL
418
     * @param ClientEntityInterface $client
419
     * @param string $userIdentifier
420
     * @param string $redirectUri
421
     * @param ScopeEntityInterface[] $scopes
422
     *
423
     * @throws OAuthServerException
424
     * @throws UniqueTokenIdentifierConstraintViolationException
425
     *
426
     * @return AuthCodeEntityInterface
427
     */
428
    protected function issueAuthCode(
429
        \DateInterval $authCodeTTL,
430
        ClientEntityInterface $client,
431
        $userIdentifier,
432
        $redirectUri,
433
        array $scopes = []
434
    )
435
    {
436
        $maxGenerationAttempts = self::MAX_RANDOM_TOKEN_GENERATION_ATTEMPTS;
437
438
        $authCode = $this->authCodeRepository->getNewAuthCode();
439
        $authCode->setExpiryDateTime((new \DateTime())->add($authCodeTTL));
440
        $authCode->setClient($client);
441
        $authCode->setUserIdentifier($userIdentifier);
442
        $authCode->setRedirectUri($redirectUri);
443
444
        foreach ($scopes as $scope) {
445
            $authCode->addScope($scope);
446
        }
447
448
        while ($maxGenerationAttempts-- > 0) {
449
            $authCode->setIdentifier($this->generateUniqueIdentifier());
450
            try {
451
                $this->authCodeRepository->persistNewAuthCode($authCode);
452
453
                return $authCode;
454
            } catch (UniqueTokenIdentifierConstraintViolationException $e) {
455
                if ($maxGenerationAttempts === 0) {
456
                    throw $e;
457
                }
458
            }
459
        }
460
    }
461
462
    /**
463
     * @param AccessTokenEntityInterface $accessToken
464
     *
465
     * @throws OAuthServerException
466
     * @throws UniqueTokenIdentifierConstraintViolationException
467
     *
468
     * @return RefreshTokenEntityInterface
469
     */
470
    protected function issueRefreshToken(AccessTokenEntityInterface $accessToken)
471
    {
472
        $maxGenerationAttempts = self::MAX_RANDOM_TOKEN_GENERATION_ATTEMPTS;
473
474
        $refreshToken = $this->refreshTokenRepository->getNewRefreshToken();
475
        $refreshToken->setExpiryDateTime((new \DateTime())->add($this->refreshTokenTTL));
476
        $refreshToken->setAccessToken($accessToken);
477
478
        while ($maxGenerationAttempts-- > 0) {
479
            $refreshToken->setIdentifier($this->generateUniqueIdentifier());
480
            try {
481
                $this->refreshTokenRepository->persistNewRefreshToken($refreshToken);
482
483
                return $refreshToken;
484
            } catch (UniqueTokenIdentifierConstraintViolationException $e) {
485
                if ($maxGenerationAttempts === 0) {
486
                    throw $e;
487
                }
488
            }
489
        }
490
    }
491
492
    /**
493
     * Generate a new unique identifier.
494
     *
495
     * @param int $length
496
     *
497
     * @throws OAuthServerException
498
     *
499
     * @return string
500
     */
501
    protected function generateUniqueIdentifier($length = 40)
502
    {
503
        try {
504
            return bin2hex(random_bytes($length));
505
            // @codeCoverageIgnoreStart
506
        } catch (\TypeError $e) {
507
            throw OAuthServerException::serverError('An unexpected error has occurred');
508
        } catch (\Error $e) {
509
            throw OAuthServerException::serverError('An unexpected error has occurred');
510
        } catch (\Exception $e) {
511
            // If you get this message, the CSPRNG failed hard.
512
            throw OAuthServerException::serverError('Could not generate a random string');
513
        }
514
        // @codeCoverageIgnoreEnd
515
    }
516
517
    /**
518
     * {@inheritdoc}
519
     */
520
    public function canRespondToAccessTokenRequest(ServerRequestInterface $request)
521
    {
522
        $requestParameters = (array)$request->getParsedBody();
523
524
        return (
525
            array_key_exists('grant_type', $requestParameters)
526
            && $requestParameters['grant_type'] === $this->getIdentifier()
527
        );
528
    }
529
530
    /**
531
     * {@inheritdoc}
532
     */
533
    public function canRespondToAuthorizationRequest(ServerRequestInterface $request)
534
    {
535
        return false;
536
    }
537
538
    /**
539
     * {@inheritdoc}
540
     */
541
    public function validateAuthorizationRequest(ServerRequestInterface $request)
542
    {
543
        throw new \LogicException('This grant cannot validate an authorization request');
544
    }
545
546
    /**
547
     * {@inheritdoc}
548
     */
549
    public function completeAuthorizationRequest(AuthorizationRequest $authorizationRequest)
550
    {
551
        throw new \LogicException('This grant cannot complete an authorization request');
552
    }
553
}
554