AuthCodeGrant::validateAuthorizationRequest()   C
last analyzed

Complexity

Conditions 14
Paths 26

Size

Total Lines 88
Code Lines 51

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 49
CRAP Score 14.9544

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 14
eloc 51
c 1
b 0
f 0
nc 26
nop 1
dl 0
loc 88
ccs 49
cts 59
cp 0.8305
crap 14.9544
rs 6.2666

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