Passed
Push — master ( dd22d8...f84357 )
by Andrew
23:07 queued 21:07
created

AuthCodeGrant::validateAuthorizationRequest()   C

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