Passed
Pull Request — master (#1470)
by
unknown
30:39
created

AuthCodeGrant::respondToAccessTokenRequest()   F

Complexity

Conditions 20
Paths 4628

Size

Total Lines 120
Code Lines 63

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 56
CRAP Score 20.3628

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 20
eloc 63
nc 4628
nop 3
dl 0
loc 120
ccs 56
cts 62
cp 0.9032
crap 20.3628
rs 0
c 2
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
/**
4
 * @author      Alex Bilbie <[email protected]>
5
 * @copyright   Copyright (c) Alex Bilbie
6
 * @license     http://mit-license.org/
7
 *
8
 * @link        https://github.com/thephpleague/oauth2-server
9
 */
10
11
declare(strict_types=1);
12
13
namespace League\OAuth2\Server\Grant;
14
15
use DateInterval;
16
use DateTimeImmutable;
17
use Exception;
18
use InvalidArgumentException;
19
use League\OAuth2\Server\CodeChallengeVerifiers\CodeChallengeVerifierInterface;
20
use League\OAuth2\Server\CodeChallengeVerifiers\PlainVerifier;
21
use League\OAuth2\Server\CodeChallengeVerifiers\S256Verifier;
22
use League\OAuth2\Server\Entities\AuthCodeEntityInterface;
23
use League\OAuth2\Server\Entities\ClientEntityInterface;
24
use League\OAuth2\Server\Entities\UserEntityInterface;
25
use League\OAuth2\Server\Exception\OAuthServerException;
26
use League\OAuth2\Server\Repositories\AuthCodeRepositoryInterface;
27
use League\OAuth2\Server\Repositories\RefreshTokenRepositoryInterface;
28
use League\OAuth2\Server\RequestAccessTokenEvent;
29
use League\OAuth2\Server\RequestEvent;
30
use League\OAuth2\Server\RequestRefreshTokenEvent;
31
use League\OAuth2\Server\RequestTypes\AuthorizationRequestInterface;
32
use League\OAuth2\Server\ResponseTypes\RedirectResponse;
33
use League\OAuth2\Server\ResponseTypes\ResponseTypeInterface;
34
use LogicException;
35
use Psr\Http\Message\ServerRequestInterface;
36
use stdClass;
37
38
use function array_key_exists;
39
use function array_keys;
40
use function array_map;
41
use function count;
42
use function hash_algos;
43
use function implode;
44
use function in_array;
45
use function is_array;
46
use function json_decode;
47
use function json_encode;
48
use function preg_match;
49
use function property_exists;
50
use function sprintf;
51
use function time;
52
53
class AuthCodeGrant extends AbstractAuthorizeGrant
54
{
55
    private bool $requireCodeChallengeForPublicClients = true;
56
57
    /**
58
     * @var CodeChallengeVerifierInterface[]
59
     */
60
    private array $codeChallengeVerifiers = [];
61
62
    /**
63
     * @throws Exception
64 53
     */
65
    public function __construct(
66
        AuthCodeRepositoryInterface $authCodeRepository,
67
        RefreshTokenRepositoryInterface $refreshTokenRepository,
68
        private DateInterval $authCodeTTL
69 53
    ) {
70 53
        $this->setAuthCodeRepository($authCodeRepository);
71 53
        $this->setRefreshTokenRepository($refreshTokenRepository);
72
        $this->refreshTokenTTL = new DateInterval('P1M');
73 53
74 53
        if (in_array('sha256', hash_algos(), true)) {
75 53
            $s256Verifier = new S256Verifier();
76
            $this->codeChallengeVerifiers[$s256Verifier->getMethod()] = $s256Verifier;
77
        }
78 53
79 53
        $plainVerifier = new PlainVerifier();
80
        $this->codeChallengeVerifiers[$plainVerifier->getMethod()] = $plainVerifier;
81
    }
82
83
    /**
84
     * Disable the requirement for a code challenge for public clients.
85
     */
86
    public function disableRequireCodeChallengeForPublicClients(): void
87
    {
88
        $this->requireCodeChallengeForPublicClients = false;
89
    }
90
91
    /**
92
     * Respond to an access token request.
93
     *
94
     * @throws OAuthServerException
95 27
     */
96
    public function respondToAccessTokenRequest(
97
        ServerRequestInterface $request,
98
        ResponseTypeInterface $responseType,
99
        DateInterval $accessTokenTTL
100 27
    ): ResponseTypeInterface {
101
        list($clientId) = $this->getClientCredentials($request);
102 27
103
        $client = $this->getClientEntityOrFail($clientId, $request);
104
105 27
        // Only validate the client if it is confidential
106 18
        if ($client->isConfidential()) {
107
            $this->validateClient($request);
108
        }
109 27
110
        $code = $this->getRequestParameter('code', $request);
111 26
112 1
        if ($code === null) {
113
            throw OAuthServerException::invalidRequest('code');
114
        }
115
116 25
        if ($this->canUseCrypt()) {
117
            try {
118 23
                $authCodePayload = json_decode($this->decrypt($code));
119
120 16
                $ace = $this->authCodeRepository->getNewAuthCode();
121 16
122 16
                if ($ace == null) {
123 16
                    // Probably should throw an exception here instead
124 16
                    return $responseType;
125 16
                }
126 16
127 9
                if (isset($authCodePayload->auth_code_id))
128 1
                    $ace->setIdentifier($authCodePayload->auth_code_id);
129 8
130 1
                if (isset($authCodePayload->client_id))
131
                    $ace->setClient($this->getClientEntityOrFail($authCodePayload->client_id, $request));
132
133 16
                if (isset($authCodePayload->user_id))
134
                    $ace->setUserIdentifier((string)$authCodePayload->user_id);
135
136 16
                if (isset($authCodePayload->code_challenge))
137 1
                    $ace->setCodeChallenge($authCodePayload->code_challenge);
138 1
139 1
                if (isset($authCodePayload->code_challenge_method))
140 1
                    $ace->setCodeChallengeMethod($authCodePayload->code_challenge_method);
141
142
                if (isset($authCodePayload->redirect_uri))
143 15
                    $ace->setRedirectUri($authCodePayload->redirect_uri);
144 7
145
                if (isset($authCodePayload->expire_time)) {
146
                    $expire = new DateTimeImmutable();
147
                    $expire = $expire->setTimestamp($authCodePayload->expire_time);
148 10
149 10
                    $ace->setExpiryDateTime($expire);
150 10
                }
151
152
                if (isset($authCodePayload->scopes)) {
153 10
                    $scopes = $this->validateScopes($authCodePayload->scopes);
154
155 8
                    $ace->setScopes($scopes);
156 7
                }
157 7
158
                
159
                
160
            } catch (InvalidArgumentException $e) {
161 8
                throw OAuthServerException::invalidGrant('Cannot validate the provided authorization code');
162
            } catch (LogicException $e) {
163 8
                throw OAuthServerException::invalidRequest('code', 'Issue decrypting the authorization code', $e);
164
            }
165
        }
166 7
        else {
167
            // Get the Auth Code Payload from Repository
168 7
            $ace = $this->authCodeRepository->getAuthCodeEntity($code);
169 1
170
            if (empty($ace)) {
171
                throw OAuthServerException::invalidRequest('code', 'Cannot find authorization code');
172
            }
173
        }
174 6
175 3
        $this->validateAuthorizationCode($ace, $client, $request);
176 3
177 3
        $scopes = $this->scopeRepository->finalizeScopes(
178 3
            $ace->getScopes(),
179
            $this->getIdentifier(),
180
            $client,
181 3
            $ace->getUserIdentifier(),
182 3
            $ace->getIdentifier()
183 3
        );
184
185 3
        $codeVerifier = $this->getRequestParameter('code_verifier', $request);
186 1
187
        // If a code challenge isn't present but a code verifier is, reject the request to block PKCE downgrade attack
188
        if ($ace->getCodeChallenge() === null && $codeVerifier !== null) {
189
            throw OAuthServerException::invalidRequest(
190
                'code_challenge',
191
                'code_verifier received when no code_challenge is present'
192
            );
193
        }
194
195
        if ($ace->getCodeChallenge() !== null) {
196
            $this->validateCodeChallenge($ace, $codeVerifier);
197
        }
198
199
        // Issue and persist new access token
200
        $accessToken = $this->issueAccessToken($accessTokenTTL, $client, $ace->getUserIdentifier(), $scopes);
201
        $this->getEmitter()->emit(new RequestAccessTokenEvent(RequestEvent::ACCESS_TOKEN_ISSUED, $request, $accessToken));
202 23
        $responseType->setAccessToken($accessToken);
203
204
        // Issue and persist new refresh token if given
205
        $refreshToken = $this->issueRefreshToken($accessToken);
206
207 23
        if ($refreshToken !== null) {
208 1
            $this->getEmitter()->emit(new RequestRefreshTokenEvent(RequestEvent::REFRESH_TOKEN_ISSUED, $request, $refreshToken));
209
            $responseType->setRefreshToken($refreshToken);
210
        }
211 22
212 1
        // Revoke used auth code
213
        $this->authCodeRepository->revokeAuthCode($ace->getIdentifier());
214
215 21
        return $responseType;
216 1
    }
217
218
    private function validateCodeChallenge(AuthCodeEntityInterface $authCodeEntity, ?string $codeVerifier): void
219 20
    {
220 1
        if ($codeVerifier === null) {
221
            throw OAuthServerException::invalidRequest('code_verifier');
222
        }
223
224
        // Validate code_verifier according to RFC-7636
225 19
        // @see: https://tools.ietf.org/html/rfc7636#section-4.1
226 19
        if (preg_match('/^[A-Za-z0-9-._~]{43,128}$/', $codeVerifier) !== 1) {
227 1
            throw OAuthServerException::invalidRequest(
228
                'code_verifier',
229
                'Code Verifier must follow the specifications of RFC-7636.'
230
            );
231 18
        }
232 2
233
234
        if (isset($this->codeChallengeVerifiers[$authCodeEntity->getCodeChallengeMethod()])) {
235
            $codeChallengeVerifier = $this->codeChallengeVerifiers[$authCodeEntity->getCodeChallengeMethod()];
236
237
            if ($authCodeEntity->getCodeChallenge() === null || $codeChallengeVerifier->verifyCodeChallenge($codeVerifier, $authCodeEntity->getCodeChallenge()) === false) {
238
                throw OAuthServerException::invalidGrant('Failed to verify `code_verifier`.');
239 33
            }
240
        } else {
241 33
            throw OAuthServerException::serverError(
242
                sprintf(
243
                    'Unsupported code challenge method `%s`',
244
                    $authCodeEntity->getCodeChallengeMethod()
245
                )
246
            );
247 2
        }
248
    }
249 2
250 2
    /**
251 2
     * Validate the authorization code.
252 2
     */
253 2
    private function validateAuthorizationCode(
254
        AuthCodeEntityInterface $authCodeEntity,
255
        ClientEntityInterface $client,
256
        ServerRequestInterface $request
257
    ): void {
258
259 16
        try {
260
            if (empty($authCodeEntity->getIdentifier())) {
261 16
                // Make sure its not empty
262 16
                throw OAuthServerException::invalidRequest('code', 'Authorization code malformed');
263 16
            }
264 16
        } catch (\Throwable $th) {
265 16
            // $identifier must not be accessed before initialization
266
            throw OAuthServerException::invalidRequest('code', 'Authorization code malformed');
267 16
        }
268 1
269
        if (time() > $authCodeEntity->getExpiryDateTime()->getTimestamp()) {
270
            throw OAuthServerException::invalidGrant('Authorization code has expired');
271 15
        }
272
273 14
        if ($this->authCodeRepository->isAuthCodeRevoked($authCodeEntity->getIdentifier()) === true) {
274
            throw OAuthServerException::invalidGrant('Authorization code has been revoked');
275 14
        }
276 12
277
        if ($authCodeEntity->getClient()->getIdentifier() !== $client->getIdentifier()) {
278 2
            throw OAuthServerException::invalidRequest('code', 'Authorization code was not issued to this client');
279 2
        }
280
281
        // The redirect URI is required in this request if it was specified
282
        // in the authorization request
283
        $redirectUri = $this->getRequestParameter('redirect_uri', $request);
284
        if ($authCodeEntity->getRedirectUri() !== null && $redirectUri === null) {
285
            throw OAuthServerException::invalidRequest('redirect_uri');
286 12
        }
287
288 12
        // If a redirect URI has been provided ensure it matches the stored redirect URI
289 12
        if ($redirectUri !== null && $authCodeEntity->getRedirectUri() !== $redirectUri) {
290 12
            throw OAuthServerException::invalidRequest('redirect_uri', 'Invalid redirect URI');
291 12
        }
292 12
    }
293 12
294 12
    /**
295
     * Return the grant identifier that can be used in matching up requests.
296 7
     */
297 7
    public function getIdentifier(): string
298 7
    {
299 7
        return 'authorization_code';
300
    }
301 7
302 1
    /**
303
     * {@inheritdoc}
304
     */
305 7
    public function canRespondToAuthorizationRequest(ServerRequestInterface $request): bool
306
    {
307 7
        return (
308
            array_key_exists('response_type', $request->getQueryParams())
309 7
            && $request->getQueryParams()['response_type'] === 'code'
310 2
            && isset($request->getQueryParams()['client_id'])
311
        );
312 2
    }
313
314
    /**
315
     * {@inheritdoc}
316
     */
317
    public function validateAuthorizationRequest(ServerRequestInterface $request): AuthorizationRequestInterface
318
    {
319 2
        $clientId = $this->getQueryStringParameter(
320 1
            'client_id',
321 1
            $request,
322 1
            $this->getServerParameter('PHP_AUTH_USER', $request)
323 1
        );
324 1
325 1
        if ($clientId === null) {
326 1
            throw OAuthServerException::invalidRequest('client_id');
327 1
        }
328 1
329
        $client = $this->getClientEntityOrFail($clientId, $request);
330
331
        $redirectUri = $this->getQueryStringParameter('redirect_uri', $request);
332
333 1
        if ($redirectUri !== null) {
334
            $this->validateRedirectUri($redirectUri, $client, $request);
335
        } elseif (
336
            $client->getRedirectUri() === '' ||
337
            (is_array($client->getRedirectUri()) && count($client->getRedirectUri()) !== 1)
338
        ) {
339
            $this->getEmitter()->emit(new RequestEvent(RequestEvent::CLIENT_AUTHENTICATION_FAILED, $request));
340 1
341 1
            throw OAuthServerException::invalidClient($request);
342 5
        }
343 1
344
        $stateParameter = $this->getQueryStringParameter('state', $request);
345
346 5
        $scopes = $this->validateScopes(
347
            $this->getQueryStringParameter('scope', $request, $this->defaultScope),
348
            $this->makeRedirectUri(
349
                $redirectUri ?? $this->getClientRedirectUri($client),
350
                $stateParameter !== null ? ['state' => $stateParameter] : []
351
            )
352 8
        );
353
354 8
        $authorizationRequest = $this->createAuthorizationRequest();
355 1
        $authorizationRequest->setGrantTypeId($this->getIdentifier());
356
        $authorizationRequest->setClient($client);
357
        $authorizationRequest->setRedirectUri($redirectUri);
358 7
359 7
        if ($stateParameter !== null) {
360
            $authorizationRequest->setState($stateParameter);
361
        }
362 7
363 6
        $authorizationRequest->setScopes($scopes);
364 6
365 6
        $codeChallenge = $this->getQueryStringParameter('code_challenge', $request);
366 6
367 6
        if ($codeChallenge !== null) {
368 6
            $codeChallengeMethod = $this->getQueryStringParameter('code_challenge_method', $request, 'plain');
369 6
370
            if ($codeChallengeMethod === null) {
371 4
                throw OAuthServerException::invalidRequest(
372 4
                    'code_challenge_method',
373 4
                    'Code challenge method must be provided when `code_challenge` is set.'
374 4
                );
375 4
            }
376 4
377 4
            if (array_key_exists($codeChallengeMethod, $this->codeChallengeVerifiers) === false) {
378 4
                throw OAuthServerException::invalidRequest(
379 4
                    'code_challenge_method',
380 4
                    'Code challenge method must be one of ' . implode(', ', array_map(
381
                        function ($method) {
382 4
                            return '`' . $method . '`';
383
                        },
384 4
                        array_keys($this->codeChallengeVerifiers)
385
                    ))
386
                );
387
            }
388 4
389 4
            // Validate code_challenge according to RFC-7636
390 4
            // @see: https://tools.ietf.org/html/rfc7636#section-4.2
391 4
            if (preg_match('/^[A-Za-z0-9-._~]{43,128}$/', $codeChallenge) !== 1) {
392 4
                throw OAuthServerException::invalidRequest(
393 4
                    'code_challenge',
394 4
                    'Code challenge must follow the specifications of RFC-7636.'
395 4
                );
396 4
            }
397 4
398
            $authorizationRequest->setCodeChallenge($codeChallenge);
399 4
            $authorizationRequest->setCodeChallengeMethod($codeChallengeMethod);
400
        } elseif ($this->requireCodeChallengeForPublicClients && !$client->isConfidential()) {
401
            throw OAuthServerException::invalidRequest('code_challenge', 'Code challenge must be provided for public clients');
402
        }
403 1
404 1
        return $authorizationRequest;
405 1
    }
406 1
407 1
    /**
408 1
     * {@inheritdoc}
409 1
     */
410 1
    public function completeAuthorizationRequest(AuthorizationRequestInterface $authorizationRequest): ResponseTypeInterface
411 1
    {
412
        if ($authorizationRequest->getUser() instanceof UserEntityInterface === false) {
413
            throw new LogicException('An instance of UserEntityInterface should be set on the AuthorizationRequest');
414
        }
415
416
        $finalRedirectUri = $authorizationRequest->getRedirectUri()
417
                          ?? $this->getClientRedirectUri($authorizationRequest->getClient());
418
419
        // The user approved the client, redirect them back with an auth code
420
        if ($authorizationRequest->isAuthorizationApproved() === true) {
421
            $authCode = $this->issueAuthCode(
422
                $this->authCodeTTL,
423
                $authorizationRequest->getClient(),
424
                $authorizationRequest->getUser()->getIdentifier(),
425
                $authorizationRequest->getRedirectUri(),
426
                $authorizationRequest->getScopes(),
427
                $authorizationRequest->getCodeChallenge(),
428
                $authorizationRequest->getCodeChallengeMethod()
429
            );
430
431
            $code = $authCode->getIdentifier();
432
433
            if ($this->canUseCrypt()) {
434
                $payload = [
435
                    'client_id'             => $authCode->getClient()->getIdentifier(),
436
                    'redirect_uri'          => $authCode->getRedirectUri(),
437
                    'auth_code_id'          => $authCode->getIdentifier(),
438
                    'scopes'                => $authCode->getScopes(),
439
                    'user_id'               => $authCode->getUserIdentifier(),
440
                    'expire_time'           => (new DateTimeImmutable())->add($this->authCodeTTL)->getTimestamp(),
441
                    'code_challenge'        => $authorizationRequest->getCodeChallenge(),
442
                    'code_challenge_method' => $authorizationRequest->getCodeChallengeMethod(),
443
                ];
444
    
445
                $jsonPayload = json_encode($payload);
446
    
447
                if ($jsonPayload === false) {
448
                    throw new LogicException('An error was encountered when JSON encoding the authorization request response');
449
                }
450
451
                $code = $this->encrypt($jsonPayload);
452
            }
453
            
454
455
            $response = new RedirectResponse();
456
            $response->setRedirectUri(
457
                $this->makeRedirectUri(
458
                    $finalRedirectUri,
459
                    [
460
                        'code'  => $code,
461
                        'state' => $authorizationRequest->getState(),
462
                    ]
463
                )
464
            );
465
466
            return $response;
467
        }
468
469
        // The user denied the client, redirect them back with an error
470
        throw OAuthServerException::accessDenied(
471
            'The user denied the request',
472
            $this->makeRedirectUri(
473
                $finalRedirectUri,
474
                [
475
                    'state' => $authorizationRequest->getState(),
476
                ]
477
            )
478
        );
479
    }
480
}
481