Completed
Pull Request — master (#788)
by
unknown
33:41
created

AbstractGrant::issueAccessToken()   B

Complexity

Conditions 5
Paths 8

Size

Total Lines 31
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 31
rs 8.439
c 0
b 0
f 0
cc 5
eloc 20
nc 8
nop 4
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
        $this->checkRedirectUri($this->getRequestParameter('redirect_uri', $request, null), $client, $request);
185
186
        return $client;
187
    }
188
189
    /**
190
     * Validate redirect Uri
191
     *
192
     * @param $redirectUri
193
     * @param $client
194
     * @param $request
195
     * @throws OAuthServerException
196
     */
197
    public function checkRedirectUri($redirectUri, $client, $request){
198
        if ($redirectUri !== null) {
199
            if (is_string($client->getRedirectUri())) {
200
                if ((strcmp($client->getRedirectUri(), $redirectUri) !== 0)) {
201
                    $registered_domain = $this->getDomain($client->getRedirectUri());
202
                    $requested_domain = $this->getDomain($redirectUri, count(explode('.', $registered_domain)));
203
                    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...
204
                        $this->getEmitter()->emit(new RequestEvent(RequestEvent::CLIENT_AUTHENTICATION_FAILED, $request));
205
                        throw OAuthServerException::invalidClient();
206
                    }
207
                }
208
209
            } elseif (
210
                is_array($client->getRedirectUri())
211
                && in_array($redirectUri, $client->getRedirectUri()) === false
212
            ) {
213
                $invalid_client = true;
214
                foreach ($client->getRedirectUri() as $url) {
215
                    $registered_domain = $this->getDomain($url);
216
                    $requested_domain = $this->getDomain($redirectUri, count(explode('.', $registered_domain)));
217
                    if (($registered_domain && $requested_domain) && $registered_domain == $requested_domain) {
218
                        $invalid_client = false;
219
                        break;
220
                    }
221
                }
222
                if ($invalid_client) {
223
                    $this->getEmitter()->emit(new RequestEvent(RequestEvent::CLIENT_AUTHENTICATION_FAILED, $request));
224
                    throw OAuthServerException::invalidClient();
225
                }
226
            }
227
        }
228
    }
229
230
    /**
231
     * Get domain from the URL
232
     *
233
     * @param $redirectUri
234
     * @param int $sub_domain_depth (2 - domain.com, 3 - subdomain.domain.com, etc.)
235
     * @return null|string
236
     */
237
    protected function getDomain($redirectUri, $sub_domain_depth = null)
238
    {
239
        try {
240
            $host = parse_url($redirectUri)['host'];
241
            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...
242
                $url_array = explode('.', $host);
243
                array_splice($url_array, 0, -$sub_domain_depth);
244
                $host = implode(".", $url_array);
245
            }
246
            return $host;
247
        } catch (\Exception $exception) {
248
            return null;
249
        }
250
    }
251
252
253
    /**
254
     * Validate scopes in the request.
255
     *
256
     * @param string $scopes
257
     * @param string $redirectUri
258
     *
259
     * @throws OAuthServerException
260
     *
261
     * @return ScopeEntityInterface[]
262
     */
263
    public function validateScopes(
264
        $scopes,
265
        $redirectUri = null
266
    )
267
    {
268
        $scopesList = array_filter(
269
            explode(self::SCOPE_DELIMITER_STRING, trim($scopes)),
270
            function ($scope) {
271
                return !empty($scope);
272
            }
273
        );
274
275
        $scopes = [];
276
        foreach ($scopesList as $scopeItem) {
277
            $scope = $this->scopeRepository->getScopeEntityByIdentifier($scopeItem);
278
279
            if ($scope instanceof ScopeEntityInterface === false) {
280
                throw OAuthServerException::invalidScope($scopeItem, $redirectUri);
281
            }
282
283
            $scopes[] = $scope;
284
        }
285
286
        return $scopes;
287
    }
288
289
    /**
290
     * Retrieve request parameter.
291
     *
292
     * @param string $parameter
293
     * @param ServerRequestInterface $request
294
     * @param mixed $default
295
     *
296
     * @return null|string
297
     */
298
    protected function getRequestParameter($parameter, ServerRequestInterface $request, $default = null)
299
    {
300
        $requestParameters = (array)$request->getParsedBody();
301
302
        return isset($requestParameters[$parameter]) ? $requestParameters[$parameter] : $default;
303
    }
304
305
    /**
306
     * Retrieve HTTP Basic Auth credentials with the Authorization header
307
     * of a request. First index of the returned array is the username,
308
     * second is the password (so list() will work). If the header does
309
     * not exist, or is otherwise an invalid HTTP Basic header, return
310
     * [null, null].
311
     *
312
     * @param ServerRequestInterface $request
313
     *
314
     * @return string[]|null[]
315
     */
316
    protected function getBasicAuthCredentials(ServerRequestInterface $request)
317
    {
318
        if (!$request->hasHeader('Authorization')) {
319
            return [null, null];
320
        }
321
322
        $header = $request->getHeader('Authorization')[0];
323
        if (strpos($header, 'Basic ') !== 0) {
324
            return [null, null];
325
        }
326
327
        if (!($decoded = base64_decode(substr($header, 6)))) {
328
            return [null, null];
329
        }
330
331
        if (strpos($decoded, ':') === false) {
332
            return [null, null]; // HTTP Basic header without colon isn't valid
333
        }
334
335
        return explode(':', $decoded, 2);
336
    }
337
338
    /**
339
     * Retrieve query string parameter.
340
     *
341
     * @param string $parameter
342
     * @param ServerRequestInterface $request
343
     * @param mixed $default
344
     *
345
     * @return null|string
346
     */
347
    protected function getQueryStringParameter($parameter, ServerRequestInterface $request, $default = null)
348
    {
349
        return isset($request->getQueryParams()[$parameter]) ? $request->getQueryParams()[$parameter] : $default;
350
    }
351
352
    /**
353
     * Retrieve cookie parameter.
354
     *
355
     * @param string $parameter
356
     * @param ServerRequestInterface $request
357
     * @param mixed $default
358
     *
359
     * @return null|string
360
     */
361
    protected function getCookieParameter($parameter, ServerRequestInterface $request, $default = null)
362
    {
363
        return isset($request->getCookieParams()[$parameter]) ? $request->getCookieParams()[$parameter] : $default;
364
    }
365
366
    /**
367
     * Retrieve server parameter.
368
     *
369
     * @param string $parameter
370
     * @param ServerRequestInterface $request
371
     * @param mixed $default
372
     *
373
     * @return null|string
374
     */
375
    protected function getServerParameter($parameter, ServerRequestInterface $request, $default = null)
376
    {
377
        return isset($request->getServerParams()[$parameter]) ? $request->getServerParams()[$parameter] : $default;
378
    }
379
380
    /**
381
     * Issue an access token.
382
     *
383
     * @param \DateInterval $accessTokenTTL
384
     * @param ClientEntityInterface $client
385
     * @param string $userIdentifier
386
     * @param ScopeEntityInterface[] $scopes
387
     *
388
     * @throws OAuthServerException
389
     * @throws UniqueTokenIdentifierConstraintViolationException
390
     *
391
     * @return AccessTokenEntityInterface
392
     */
393
    protected function issueAccessToken(
394
        \DateInterval $accessTokenTTL,
395
        ClientEntityInterface $client,
396
        $userIdentifier,
397
        array $scopes = []
398
    )
399
    {
400
        $maxGenerationAttempts = self::MAX_RANDOM_TOKEN_GENERATION_ATTEMPTS;
401
402
        $accessToken = $this->accessTokenRepository->getNewToken($client, $scopes, $userIdentifier);
403
        $accessToken->setClient($client);
404
        $accessToken->setUserIdentifier($userIdentifier);
405
        $accessToken->setExpiryDateTime((new \DateTime())->add($accessTokenTTL));
406
407
        foreach ($scopes as $scope) {
408
            $accessToken->addScope($scope);
409
        }
410
411
        while ($maxGenerationAttempts-- > 0) {
412
            $accessToken->setIdentifier($this->generateUniqueIdentifier());
413
            try {
414
                $this->accessTokenRepository->persistNewAccessToken($accessToken);
415
416
                return $accessToken;
417
            } catch (UniqueTokenIdentifierConstraintViolationException $e) {
418
                if ($maxGenerationAttempts === 0) {
419
                    throw $e;
420
                }
421
            }
422
        }
423
    }
424
425
    /**
426
     * Issue an auth code.
427
     *
428
     * @param \DateInterval $authCodeTTL
429
     * @param ClientEntityInterface $client
430
     * @param string $userIdentifier
431
     * @param string $redirectUri
432
     * @param ScopeEntityInterface[] $scopes
433
     *
434
     * @throws OAuthServerException
435
     * @throws UniqueTokenIdentifierConstraintViolationException
436
     *
437
     * @return AuthCodeEntityInterface
438
     */
439
    protected function issueAuthCode(
440
        \DateInterval $authCodeTTL,
441
        ClientEntityInterface $client,
442
        $userIdentifier,
443
        $redirectUri,
444
        array $scopes = []
445
    )
446
    {
447
        $maxGenerationAttempts = self::MAX_RANDOM_TOKEN_GENERATION_ATTEMPTS;
448
449
        $authCode = $this->authCodeRepository->getNewAuthCode();
450
        $authCode->setExpiryDateTime((new \DateTime())->add($authCodeTTL));
451
        $authCode->setClient($client);
452
        $authCode->setUserIdentifier($userIdentifier);
453
        $authCode->setRedirectUri($redirectUri);
454
455
        foreach ($scopes as $scope) {
456
            $authCode->addScope($scope);
457
        }
458
459
        while ($maxGenerationAttempts-- > 0) {
460
            $authCode->setIdentifier($this->generateUniqueIdentifier());
461
            try {
462
                $this->authCodeRepository->persistNewAuthCode($authCode);
463
464
                return $authCode;
465
            } catch (UniqueTokenIdentifierConstraintViolationException $e) {
466
                if ($maxGenerationAttempts === 0) {
467
                    throw $e;
468
                }
469
            }
470
        }
471
    }
472
473
    /**
474
     * @param AccessTokenEntityInterface $accessToken
475
     *
476
     * @throws OAuthServerException
477
     * @throws UniqueTokenIdentifierConstraintViolationException
478
     *
479
     * @return RefreshTokenEntityInterface
480
     */
481
    protected function issueRefreshToken(AccessTokenEntityInterface $accessToken)
482
    {
483
        $maxGenerationAttempts = self::MAX_RANDOM_TOKEN_GENERATION_ATTEMPTS;
484
485
        $refreshToken = $this->refreshTokenRepository->getNewRefreshToken();
486
        $refreshToken->setExpiryDateTime((new \DateTime())->add($this->refreshTokenTTL));
487
        $refreshToken->setAccessToken($accessToken);
488
489
        while ($maxGenerationAttempts-- > 0) {
490
            $refreshToken->setIdentifier($this->generateUniqueIdentifier());
491
            try {
492
                $this->refreshTokenRepository->persistNewRefreshToken($refreshToken);
493
494
                return $refreshToken;
495
            } catch (UniqueTokenIdentifierConstraintViolationException $e) {
496
                if ($maxGenerationAttempts === 0) {
497
                    throw $e;
498
                }
499
            }
500
        }
501
    }
502
503
    /**
504
     * Generate a new unique identifier.
505
     *
506
     * @param int $length
507
     *
508
     * @throws OAuthServerException
509
     *
510
     * @return string
511
     */
512
    protected function generateUniqueIdentifier($length = 40)
513
    {
514
        try {
515
            return bin2hex(random_bytes($length));
516
            // @codeCoverageIgnoreStart
517
        } catch (\TypeError $e) {
518
            throw OAuthServerException::serverError('An unexpected error has occurred');
519
        } catch (\Error $e) {
520
            throw OAuthServerException::serverError('An unexpected error has occurred');
521
        } catch (\Exception $e) {
522
            // If you get this message, the CSPRNG failed hard.
523
            throw OAuthServerException::serverError('Could not generate a random string');
524
        }
525
        // @codeCoverageIgnoreEnd
526
    }
527
528
    /**
529
     * {@inheritdoc}
530
     */
531
    public function canRespondToAccessTokenRequest(ServerRequestInterface $request)
532
    {
533
        $requestParameters = (array)$request->getParsedBody();
534
535
        return (
536
            array_key_exists('grant_type', $requestParameters)
537
            && $requestParameters['grant_type'] === $this->getIdentifier()
538
        );
539
    }
540
541
    /**
542
     * {@inheritdoc}
543
     */
544
    public function canRespondToAuthorizationRequest(ServerRequestInterface $request)
545
    {
546
        return false;
547
    }
548
549
    /**
550
     * {@inheritdoc}
551
     */
552
    public function validateAuthorizationRequest(ServerRequestInterface $request)
553
    {
554
        throw new \LogicException('This grant cannot validate an authorization request');
555
    }
556
557
    /**
558
     * {@inheritdoc}
559
     */
560
    public function completeAuthorizationRequest(AuthorizationRequest $authorizationRequest)
561
    {
562
        throw new \LogicException('This grant cannot complete an authorization request');
563
    }
564
}
565