Completed
Pull Request — master (#787)
by
unknown
34:01
created

AbstractGrant::getDomain()   A

Complexity

Conditions 3
Paths 6

Size

Total Lines 14
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 14
rs 9.4285
c 0
b 0
f 0
cc 3
eloc 10
nc 6
nop 2
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
12
namespace League\OAuth2\Server\Grant;
13
14
use League\Event\EmitterAwareTrait;
15
use League\OAuth2\Server\CryptKey;
16
use League\OAuth2\Server\CryptTrait;
17
use League\OAuth2\Server\Entities\AccessTokenEntityInterface;
18
use League\OAuth2\Server\Entities\AuthCodeEntityInterface;
19
use League\OAuth2\Server\Entities\ClientEntityInterface;
20
use League\OAuth2\Server\Entities\RefreshTokenEntityInterface;
21
use League\OAuth2\Server\Entities\ScopeEntityInterface;
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
     * @param ClientRepositoryInterface $clientRepository
87
     */
88
    public function setClientRepository(ClientRepositoryInterface $clientRepository)
89
    {
90
        $this->clientRepository = $clientRepository;
91
    }
92
93
    /**
94
     * @param AccessTokenRepositoryInterface $accessTokenRepository
95
     */
96
    public function setAccessTokenRepository(AccessTokenRepositoryInterface $accessTokenRepository)
97
    {
98
        $this->accessTokenRepository = $accessTokenRepository;
99
    }
100
101
    /**
102
     * @param ScopeRepositoryInterface $scopeRepository
103
     */
104
    public function setScopeRepository(ScopeRepositoryInterface $scopeRepository)
105
    {
106
        $this->scopeRepository = $scopeRepository;
107
    }
108
109
    /**
110
     * @param RefreshTokenRepositoryInterface $refreshTokenRepository
111
     */
112
    public function setRefreshTokenRepository(RefreshTokenRepositoryInterface $refreshTokenRepository)
113
    {
114
        $this->refreshTokenRepository = $refreshTokenRepository;
115
    }
116
117
    /**
118
     * @param AuthCodeRepositoryInterface $authCodeRepository
119
     */
120
    public function setAuthCodeRepository(AuthCodeRepositoryInterface $authCodeRepository)
121
    {
122
        $this->authCodeRepository = $authCodeRepository;
123
    }
124
125
    /**
126
     * @param UserRepositoryInterface $userRepository
127
     */
128
    public function setUserRepository(UserRepositoryInterface $userRepository)
129
    {
130
        $this->userRepository = $userRepository;
131
    }
132
133
    /**
134
     * {@inheritdoc}
135
     */
136
    public function setRefreshTokenTTL(\DateInterval $refreshTokenTTL)
137
    {
138
        $this->refreshTokenTTL = $refreshTokenTTL;
139
    }
140
141
    /**
142
     * Set the private key
143
     *
144
     * @param \League\OAuth2\Server\CryptKey $key
145
     */
146
    public function setPrivateKey(CryptKey $key)
147
    {
148
        $this->privateKey = $key;
149
    }
150
151
    /**
152
     * Validate the client.
153
     *
154
     * @param ServerRequestInterface $request
155
     *
156
     * @throws OAuthServerException
157
     *
158
     * @return ClientEntityInterface
159
     */
160
    protected function validateClient(ServerRequestInterface $request)
161
    {
162
        list($basicAuthUser, $basicAuthPassword) = $this->getBasicAuthCredentials($request);
163
164
        $clientId = $this->getRequestParameter('client_id', $request, $basicAuthUser);
165
        if (is_null($clientId)) {
166
            throw OAuthServerException::invalidRequest('client_id');
167
        }
168
169
        // If the client is confidential require the client secret
170
        $clientSecret = $this->getRequestParameter('client_secret', $request, $basicAuthPassword);
171
172
        $client = $this->clientRepository->getClientEntity(
173
            $clientId,
174
            $this->getIdentifier(),
175
            $clientSecret,
176
            true
177
        );
178
179
        if ($client instanceof ClientEntityInterface === false) {
180
            $this->getEmitter()->emit(new RequestEvent(RequestEvent::CLIENT_AUTHENTICATION_FAILED, $request));
181
            throw OAuthServerException::invalidClient();
182
        }
183
184
        // If a redirect URI is provided ensure it matches what is pre-registered
185
        $redirectUri = $this->getRequestParameter('redirect_uri', $request, null);
186
        if ($redirectUri !== null) {
187
            if (is_string($client->getRedirectUri())) {
188
                if ((strcmp($client->getRedirectUri(), $redirectUri) !== 0)) {
189
                    $registered_domain = $this->getDomain($client->getRedirectUri());
190
                    $requested_domain = $this->getDomain($redirectUri, count(explode('.', $registered_domain)));
191
                    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...
192
                        $this->getEmitter()->emit(new RequestEvent(RequestEvent::CLIENT_AUTHENTICATION_FAILED, $request));
193
                        throw OAuthServerException::invalidClient();
194
                    }
195
                }
196
197
            } elseif (
198
                is_array($client->getRedirectUri())
199
                && in_array($redirectUri, $client->getRedirectUri()) === false
200
            ) {
201
                $invalid_client = true;
202
                foreach ($client->getRedirectUri() as $url) {
203
                    $registered_domain = $this->getDomain($url);
204
                    $requested_domain = $this->getDomain($redirectUri, count(explode('.', $registered_domain)));
205
                    if (($registered_domain && $requested_domain) && $registered_domain == $requested_domain) {
206
                        $invalid_client = false;
207
                        break;
208
                    }
209
                }
210
                if ($invalid_client) {
211
                    $this->getEmitter()->emit(new RequestEvent(RequestEvent::CLIENT_AUTHENTICATION_FAILED, $request));
212
                    throw OAuthServerException::invalidClient();
213
                }
214
            }
215
        }
216
217
        return $client;
218
    }
219
220
    /**
221
     * Get domain from the URL
222
     *
223
     * @param $redirectUri
224
     * @param int $sub_domain_depth (2 - domain.com, 3 - subdomain.domain.com, etc.)
225
     * @return null|string
226
     */
227
    protected function getDomain($redirectUri, $sub_domain_depth = null)
228
    {
229
        try {
230
            $host = parse_url($redirectUri)['host'];
231
            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...
232
                $url_array = explode('.', $host);
233
                array_splice($url_array, 0, -$sub_domain_depth);
234
                $host = implode(".", $url_array);
235
            }
236
            return $host;
237
        } catch (\Exception $exception) {
238
            return null;
239
        }
240
    }
241
242
243
    /**
244
     * Validate scopes in the request.
245
     *
246
     * @param string $scopes
247
     * @param string $redirectUri
248
     *
249
     * @throws OAuthServerException
250
     *
251
     * @return ScopeEntityInterface[]
252
     */
253
    public function validateScopes(
254
        $scopes,
255
        $redirectUri = null
256
    )
257
    {
258
        $scopesList = array_filter(
259
            explode(self::SCOPE_DELIMITER_STRING, trim($scopes)),
260
            function ($scope) {
261
                return !empty($scope);
262
            }
263
        );
264
265
        $scopes = [];
266
        foreach ($scopesList as $scopeItem) {
267
            $scope = $this->scopeRepository->getScopeEntityByIdentifier($scopeItem);
268
269
            if ($scope instanceof ScopeEntityInterface === false) {
270
                throw OAuthServerException::invalidScope($scopeItem, $redirectUri);
271
            }
272
273
            $scopes[] = $scope;
274
        }
275
276
        return $scopes;
277
    }
278
279
    /**
280
     * Retrieve request parameter.
281
     *
282
     * @param string $parameter
283
     * @param ServerRequestInterface $request
284
     * @param mixed $default
285
     *
286
     * @return null|string
287
     */
288
    protected function getRequestParameter($parameter, ServerRequestInterface $request, $default = null)
289
    {
290
        $requestParameters = (array)$request->getParsedBody();
291
292
        return isset($requestParameters[$parameter]) ? $requestParameters[$parameter] : $default;
293
    }
294
295
    /**
296
     * Retrieve HTTP Basic Auth credentials with the Authorization header
297
     * of a request. First index of the returned array is the username,
298
     * second is the password (so list() will work). If the header does
299
     * not exist, or is otherwise an invalid HTTP Basic header, return
300
     * [null, null].
301
     *
302
     * @param ServerRequestInterface $request
303
     *
304
     * @return string[]|null[]
305
     */
306
    protected function getBasicAuthCredentials(ServerRequestInterface $request)
307
    {
308
        if (!$request->hasHeader('Authorization')) {
309
            return [null, null];
310
        }
311
312
        $header = $request->getHeader('Authorization')[0];
313
        if (strpos($header, 'Basic ') !== 0) {
314
            return [null, null];
315
        }
316
317
        if (!($decoded = base64_decode(substr($header, 6)))) {
318
            return [null, null];
319
        }
320
321
        if (strpos($decoded, ':') === false) {
322
            return [null, null]; // HTTP Basic header without colon isn't valid
323
        }
324
325
        return explode(':', $decoded, 2);
326
    }
327
328
    /**
329
     * Retrieve query string parameter.
330
     *
331
     * @param string $parameter
332
     * @param ServerRequestInterface $request
333
     * @param mixed $default
334
     *
335
     * @return null|string
336
     */
337
    protected function getQueryStringParameter($parameter, ServerRequestInterface $request, $default = null)
338
    {
339
        return isset($request->getQueryParams()[$parameter]) ? $request->getQueryParams()[$parameter] : $default;
340
    }
341
342
    /**
343
     * Retrieve cookie parameter.
344
     *
345
     * @param string $parameter
346
     * @param ServerRequestInterface $request
347
     * @param mixed $default
348
     *
349
     * @return null|string
350
     */
351
    protected function getCookieParameter($parameter, ServerRequestInterface $request, $default = null)
352
    {
353
        return isset($request->getCookieParams()[$parameter]) ? $request->getCookieParams()[$parameter] : $default;
354
    }
355
356
    /**
357
     * Retrieve server parameter.
358
     *
359
     * @param string $parameter
360
     * @param ServerRequestInterface $request
361
     * @param mixed $default
362
     *
363
     * @return null|string
364
     */
365
    protected function getServerParameter($parameter, ServerRequestInterface $request, $default = null)
366
    {
367
        return isset($request->getServerParams()[$parameter]) ? $request->getServerParams()[$parameter] : $default;
368
    }
369
370
    /**
371
     * Issue an access token.
372
     *
373
     * @param \DateInterval $accessTokenTTL
374
     * @param ClientEntityInterface $client
375
     * @param string $userIdentifier
376
     * @param ScopeEntityInterface[] $scopes
377
     *
378
     * @throws OAuthServerException
379
     * @throws UniqueTokenIdentifierConstraintViolationException
380
     *
381
     * @return AccessTokenEntityInterface
382
     */
383
    protected function issueAccessToken(
384
        \DateInterval $accessTokenTTL,
385
        ClientEntityInterface $client,
386
        $userIdentifier,
387
        array $scopes = []
388
    )
389
    {
390
        $maxGenerationAttempts = self::MAX_RANDOM_TOKEN_GENERATION_ATTEMPTS;
391
392
        $accessToken = $this->accessTokenRepository->getNewToken($client, $scopes, $userIdentifier);
393
        $accessToken->setClient($client);
394
        $accessToken->setUserIdentifier($userIdentifier);
395
        $accessToken->setExpiryDateTime((new \DateTime())->add($accessTokenTTL));
396
397
        foreach ($scopes as $scope) {
398
            $accessToken->addScope($scope);
399
        }
400
401
        while ($maxGenerationAttempts-- > 0) {
402
            $accessToken->setIdentifier($this->generateUniqueIdentifier());
403
            try {
404
                $this->accessTokenRepository->persistNewAccessToken($accessToken);
405
406
                return $accessToken;
407
            } catch (UniqueTokenIdentifierConstraintViolationException $e) {
408
                if ($maxGenerationAttempts === 0) {
409
                    throw $e;
410
                }
411
            }
412
        }
413
    }
414
415
    /**
416
     * Issue an auth code.
417
     *
418
     * @param \DateInterval $authCodeTTL
419
     * @param ClientEntityInterface $client
420
     * @param string $userIdentifier
421
     * @param string $redirectUri
422
     * @param ScopeEntityInterface[] $scopes
423
     *
424
     * @throws OAuthServerException
425
     * @throws UniqueTokenIdentifierConstraintViolationException
426
     *
427
     * @return AuthCodeEntityInterface
428
     */
429
    protected function issueAuthCode(
430
        \DateInterval $authCodeTTL,
431
        ClientEntityInterface $client,
432
        $userIdentifier,
433
        $redirectUri,
434
        array $scopes = []
435
    )
436
    {
437
        $maxGenerationAttempts = self::MAX_RANDOM_TOKEN_GENERATION_ATTEMPTS;
438
439
        $authCode = $this->authCodeRepository->getNewAuthCode();
440
        $authCode->setExpiryDateTime((new \DateTime())->add($authCodeTTL));
441
        $authCode->setClient($client);
442
        $authCode->setUserIdentifier($userIdentifier);
443
        $authCode->setRedirectUri($redirectUri);
444
445
        foreach ($scopes as $scope) {
446
            $authCode->addScope($scope);
447
        }
448
449
        while ($maxGenerationAttempts-- > 0) {
450
            $authCode->setIdentifier($this->generateUniqueIdentifier());
451
            try {
452
                $this->authCodeRepository->persistNewAuthCode($authCode);
453
454
                return $authCode;
455
            } catch (UniqueTokenIdentifierConstraintViolationException $e) {
456
                if ($maxGenerationAttempts === 0) {
457
                    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
    protected function issueRefreshToken(AccessTokenEntityInterface $accessToken)
472
    {
473
        $maxGenerationAttempts = self::MAX_RANDOM_TOKEN_GENERATION_ATTEMPTS;
474
475
        $refreshToken = $this->refreshTokenRepository->getNewRefreshToken();
476
        $refreshToken->setExpiryDateTime((new \DateTime())->add($this->refreshTokenTTL));
477
        $refreshToken->setAccessToken($accessToken);
478
479
        while ($maxGenerationAttempts-- > 0) {
480
            $refreshToken->setIdentifier($this->generateUniqueIdentifier());
481
            try {
482
                $this->refreshTokenRepository->persistNewRefreshToken($refreshToken);
483
484
                return $refreshToken;
485
            } catch (UniqueTokenIdentifierConstraintViolationException $e) {
486
                if ($maxGenerationAttempts === 0) {
487
                    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
    protected function generateUniqueIdentifier($length = 40)
503
    {
504
        try {
505
            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
    public function canRespondToAccessTokenRequest(ServerRequestInterface $request)
522
    {
523
        $requestParameters = (array)$request->getParsedBody();
524
525
        return (
526
            array_key_exists('grant_type', $requestParameters)
527
            && $requestParameters['grant_type'] === $this->getIdentifier()
528
        );
529
    }
530
531
    /**
532
     * {@inheritdoc}
533
     */
534
    public function canRespondToAuthorizationRequest(ServerRequestInterface $request)
535
    {
536
        return false;
537
    }
538
539
    /**
540
     * {@inheritdoc}
541
     */
542
    public function validateAuthorizationRequest(ServerRequestInterface $request)
543
    {
544
        throw new \LogicException('This grant cannot validate an authorization request');
545
    }
546
547
    /**
548
     * {@inheritdoc}
549
     */
550
    public function completeAuthorizationRequest(AuthorizationRequest $authorizationRequest)
551
    {
552
        throw new \LogicException('This grant cannot complete an authorization request');
553
    }
554
}
555