Passed
Pull Request — master (#1470)
by
unknown
33:34
created

AuthCodeGrant::validateAuthorizationCode()   B

Complexity

Conditions 10
Paths 8

Size

Total Lines 37
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 18
CRAP Score 10.1

Importance

Changes 0
Metric Value
cc 10
eloc 16
c 0
b 0
f 0
nc 8
nop 3
dl 0
loc 37
ccs 18
cts 20
cp 0.9
crap 10.1
rs 7.6666

How to fix   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
37
use function array_key_exists;
38
use function array_keys;
39
use function array_map;
40
use function count;
41
use function hash_algos;
42
use function implode;
43
use function in_array;
44
use function is_array;
45
use function json_decode;
46
use function json_encode;
47
use function preg_match;
48
use function sprintf;
49
use function time;
50
51
class AuthCodeGrant extends AbstractAuthorizeGrant
52
{
53
    private bool $requireCodeChallengeForPublicClients = true;
54
55
    /**
56
     * @var CodeChallengeVerifierInterface[]
57
     */
58
    private array $codeChallengeVerifiers = [];
59
60
    /**
61
     * @throws Exception
62
     */
63
    public function __construct(
64 53
        AuthCodeRepositoryInterface $authCodeRepository,
65
        RefreshTokenRepositoryInterface $refreshTokenRepository,
66
        private DateInterval $authCodeTTL
67
    ) {
68
        $this->setAuthCodeRepository($authCodeRepository);
69 53
        $this->setRefreshTokenRepository($refreshTokenRepository);
70 53
        $this->refreshTokenTTL = new DateInterval('P1M');
71 53
72
        if (in_array('sha256', hash_algos(), true)) {
73 53
            $s256Verifier = new S256Verifier();
74 53
            $this->codeChallengeVerifiers[$s256Verifier->getMethod()] = $s256Verifier;
75 53
        }
76
77
        $plainVerifier = new PlainVerifier();
78 53
        $this->codeChallengeVerifiers[$plainVerifier->getMethod()] = $plainVerifier;
79 53
    }
80
81
    /**
82
     * Disable the requirement for a code challenge for public clients.
83
     */
84
    public function disableRequireCodeChallengeForPublicClients(): void
85
    {
86
        $this->requireCodeChallengeForPublicClients = false;
87
    }
88
89
    /**
90
     * Respond to an access token request.
91
     *
92
     * @throws OAuthServerException
93
     */
94
    public function respondToAccessTokenRequest(
95 27
        ServerRequestInterface $request,
96
        ResponseTypeInterface $responseType,
97
        DateInterval $accessTokenTTL
98
    ): ResponseTypeInterface {
99
        list($clientId) = $this->getClientCredentials($request);
100 27
101
        $client = $this->getClientEntityOrFail($clientId, $request);
102 27
103
        // Only validate the client if it is confidential
104
        if ($client->isConfidential()) {
105 27
            $this->validateClient($request);
106 18
        }
107
108
        $code = $this->getRequestParameter('code', $request);
109 27
110
        if ($code === null) {
111 26
            throw OAuthServerException::invalidRequest('code');
112 1
        }
113
114
        if ($this->canUseCrypt()) {
115
            try {
116 25
                $authCodePayload = json_decode($this->decrypt($code));
117
118 23
                $ace = $this->authCodeRepository->getNewAuthCode();
119
120 16
                if ($ace == null) {
121 16
                    // Probably should throw an exception here instead
122 16
                    return $responseType;
123 16
                }
124 16
125 16
                if (isset($authCodePayload->auth_code_id)) {
126 16
                    $ace->setIdentifier($authCodePayload->auth_code_id);
127 9
                }
128 1
129 8
                if (isset($authCodePayload->client_id)) {
130 1
                    $ace->setClient($this->getClientEntityOrFail($authCodePayload->client_id, $request));
131
                }
132
133 16
                if (isset($authCodePayload->user_id)) {
134
                    $ace->setUserIdentifier((string)$authCodePayload->user_id);
135
                }
136 16
137 1
                if (isset($authCodePayload->code_challenge)) {
138 1
                    $ace->setCodeChallenge($authCodePayload->code_challenge);
139 1
                }
140 1
141
                if (isset($authCodePayload->code_challenge_method)) {
142
                    $ace->setCodeChallengeMethod($authCodePayload->code_challenge_method);
143 15
                }
144 7
145
                if (isset($authCodePayload->redirect_uri)) {
146
                    $ace->setRedirectUri($authCodePayload->redirect_uri);
147
                }
148 10
149 10
                if (isset($authCodePayload->expire_time)) {
150 10
                    $expire = new DateTimeImmutable();
151
                    $expire = $expire->setTimestamp($authCodePayload->expire_time);
152
153 10
                    $ace->setExpiryDateTime($expire);
154
                }
155 8
156 7
                if (isset($authCodePayload->scopes)) {
157 7
                    $scopes = $this->validateScopes($authCodePayload->scopes);
158
159
                    $ace->setScopes($scopes);
160
                }
161 8
162
            } catch (InvalidArgumentException $e) {
163 8
                throw OAuthServerException::invalidGrant('Cannot validate the provided authorization code');
164
            } catch (LogicException $e) {
165
                throw OAuthServerException::invalidRequest('code', 'Issue decrypting the authorization code', $e);
166 7
            }
167
        } else {
168 7
            // Get the Auth Code Payload from Repository
169 1
            $ace = $this->authCodeRepository->getAuthCodeEntity($code);
170
171
            if (empty($ace)) {
172
                throw OAuthServerException::invalidRequest('code', 'Cannot find authorization code');
173
            }
174 6
        }
175 3
176 3
        $this->validateAuthorizationCode($ace, $client, $request);
177 3
178 3
        $scopes = $this->scopeRepository->finalizeScopes(
179
            $ace->getScopes(),
180
            $this->getIdentifier(),
181 3
            $client,
182 3
            $ace->getUserIdentifier(),
183 3
            $ace->getIdentifier()
184
        );
185 3
186 1
        $codeVerifier = $this->getRequestParameter('code_verifier', $request);
187
188
        // If a code challenge isn't present but a code verifier is, reject the request to block PKCE downgrade attack
189
        if ($ace->getCodeChallenge() === null && $codeVerifier !== null) {
190
            throw OAuthServerException::invalidRequest(
191
                'code_challenge',
192
                'code_verifier received when no code_challenge is present'
193
            );
194
        }
195
196
        if ($ace->getCodeChallenge() !== null) {
197
            $this->validateCodeChallenge($ace, $codeVerifier);
198
        }
199
200
        // Issue and persist new access token
201
        $accessToken = $this->issueAccessToken($accessTokenTTL, $client, $ace->getUserIdentifier(), $scopes);
202 23
        $this->getEmitter()->emit(new RequestAccessTokenEvent(RequestEvent::ACCESS_TOKEN_ISSUED, $request, $accessToken));
203
        $responseType->setAccessToken($accessToken);
204
205
        // Issue and persist new refresh token if given
206
        $refreshToken = $this->issueRefreshToken($accessToken);
207 23
208 1
        if ($refreshToken !== null) {
209
            $this->getEmitter()->emit(new RequestRefreshTokenEvent(RequestEvent::REFRESH_TOKEN_ISSUED, $request, $refreshToken));
210
            $responseType->setRefreshToken($refreshToken);
211 22
        }
212 1
213
        // Revoke used auth code
214
        $this->authCodeRepository->revokeAuthCode($ace->getIdentifier());
215 21
216 1
        return $responseType;
217
    }
218
219 20
    private function validateCodeChallenge(AuthCodeEntityInterface $authCodeEntity, ?string $codeVerifier): void
220 1
    {
221
        if ($codeVerifier === null) {
222
            throw OAuthServerException::invalidRequest('code_verifier');
223
        }
224
225 19
        // Validate code_verifier according to RFC-7636
226 19
        // @see: https://tools.ietf.org/html/rfc7636#section-4.1
227 1
        if (preg_match('/^[A-Za-z0-9-._~]{43,128}$/', $codeVerifier) !== 1) {
228
            throw OAuthServerException::invalidRequest(
229
                'code_verifier',
230
                'Code Verifier must follow the specifications of RFC-7636.'
231 18
            );
232 2
        }
233
234
235
        if (isset($this->codeChallengeVerifiers[$authCodeEntity->getCodeChallengeMethod()])) {
236
            $codeChallengeVerifier = $this->codeChallengeVerifiers[$authCodeEntity->getCodeChallengeMethod()];
237
238
            if ($authCodeEntity->getCodeChallenge() === null || $codeChallengeVerifier->verifyCodeChallenge($codeVerifier, $authCodeEntity->getCodeChallenge()) === false) {
239 33
                throw OAuthServerException::invalidGrant('Failed to verify `code_verifier`.');
240
            }
241 33
        } else {
242
            throw OAuthServerException::serverError(
243
                sprintf(
244
                    'Unsupported code challenge method `%s`',
245
                    $authCodeEntity->getCodeChallengeMethod()
246
                )
247 2
            );
248
        }
249 2
    }
250 2
251 2
    /**
252 2
     * Validate the authorization code.
253 2
     */
254
    private function validateAuthorizationCode(
255
        AuthCodeEntityInterface $authCodeEntity,
256
        ClientEntityInterface $client,
257
        ServerRequestInterface $request
258
    ): void {
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
            $response = new RedirectResponse();
455
            $response->setRedirectUri(
456
                $this->makeRedirectUri(
457
                    $finalRedirectUri,
458
                    [
459
                        'code'  => $code,
460
                        'state' => $authorizationRequest->getState(),
461
                    ]
462
                )
463
            );
464
465
            return $response;
466
        }
467
468
        // The user denied the client, redirect them back with an error
469
        throw OAuthServerException::accessDenied(
470
            'The user denied the request',
471
            $this->makeRedirectUri(
472
                $finalRedirectUri,
473
                [
474
                    'state' => $authorizationRequest->getState(),
475
                ]
476
            )
477
        );
478
    }
479
}
480