Passed
Push — master ( d4ef04...f654cb )
by Andrew
02:07 queued 15s
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 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 3
                if (!isset($authCodePayload->code_challenge) || $codeChallengeVerifier->verifyCodeChallenge($codeVerifier, $authCodePayload->code_challenge) === false) {
179 1
                    throw OAuthServerException::invalidGrant('Failed to verify `code_verifier`.');
180
                }
181
            } else {
182
                throw OAuthServerException::serverError(
183
                    sprintf(
184
                        'Unsupported code challenge method `%s`',
185
                        $authCodePayload->code_challenge_method
186
                    )
187
                );
188
            }
189
        }
190
    }
191
192
    /**
193
     * Validate the authorization code.
194
     */
195 19
    private function validateAuthorizationCode(
196
        stdClass $authCodePayload,
197
        ClientEntityInterface $client,
198
        ServerRequestInterface $request
199
    ): void {
200 19
        if (!property_exists($authCodePayload, 'auth_code_id')) {
201 1
            throw OAuthServerException::invalidRequest('code', 'Authorization code malformed');
202
        }
203
204 18
        if (time() > $authCodePayload->expire_time) {
205 1
            throw OAuthServerException::invalidGrant('Authorization code has expired');
206
        }
207
208 17
        if ($this->authCodeRepository->isAuthCodeRevoked($authCodePayload->auth_code_id) === true) {
209 1
            throw OAuthServerException::invalidGrant('Authorization code has been revoked');
210
        }
211
212 16
        if ($authCodePayload->client_id !== $client->getIdentifier()) {
213 1
            throw OAuthServerException::invalidRequest('code', 'Authorization code was not issued to this client');
214
        }
215
216
        // The redirect URI is required in this request if it was specified
217
        // in the authorization request
218 15
        $redirectUri = $this->getRequestParameter('redirect_uri', $request);
219 15
        if ($authCodePayload->redirect_uri !== null && $redirectUri === null) {
220
            throw OAuthServerException::invalidRequest('redirect_uri');
221
        }
222
223
        // If a redirect URI has been provided ensure it matches the stored redirect URI
224 15
        if ($redirectUri !== null && $authCodePayload->redirect_uri !== $redirectUri) {
225
            throw OAuthServerException::invalidRequest('redirect_uri', 'Invalid redirect URI');
226
        }
227
    }
228
229
    /**
230
     * Return the grant identifier that can be used in matching up requests.
231
     */
232 43
    public function getIdentifier(): string
233
    {
234 43
        return 'authorization_code';
235
    }
236
237
    /**
238
     * {@inheritdoc}
239
     */
240 2
    public function canRespondToAuthorizationRequest(ServerRequestInterface $request): bool
241
    {
242 2
        return (
243 2
            array_key_exists('response_type', $request->getQueryParams())
244 2
            && $request->getQueryParams()['response_type'] === 'code'
245 2
            && isset($request->getQueryParams()['client_id'])
246 2
        );
247
    }
248
249
    /**
250
     * {@inheritdoc}
251
     */
252 16
    public function validateAuthorizationRequest(ServerRequestInterface $request): AuthorizationRequestInterface
253
    {
254 16
        $clientId = $this->getQueryStringParameter(
255 16
            'client_id',
256 16
            $request,
257 16
            $this->getServerParameter('PHP_AUTH_USER', $request)
258 16
        );
259
260 16
        if ($clientId === null) {
261 1
            throw OAuthServerException::invalidRequest('client_id');
262
        }
263
264 15
        $client = $this->getClientEntityOrFail($clientId, $request);
265
266 14
        $redirectUri = $this->getQueryStringParameter('redirect_uri', $request);
267
268 14
        if ($redirectUri !== null) {
269 12
            $this->validateRedirectUri($redirectUri, $client, $request);
270
        } elseif (
271 2
            $client->getRedirectUri() === '' ||
272 2
            (is_array($client->getRedirectUri()) && count($client->getRedirectUri()) !== 1)
273
        ) {
274
            $this->getEmitter()->emit(new RequestEvent(RequestEvent::CLIENT_AUTHENTICATION_FAILED, $request));
275
276
            throw OAuthServerException::invalidClient($request);
277
        }
278
279 12
        $stateParameter = $this->getQueryStringParameter('state', $request);
280
281 12
        $scopes = $this->validateScopes(
282 12
            $this->getQueryStringParameter('scope', $request, $this->defaultScope),
283 12
            $this->makeRedirectUri(
284 12
                $redirectUri ?? $this->getClientRedirectUri($client),
285 12
                $stateParameter !== null ? ['state' => $stateParameter] : []
286 12
            )
287 12
        );
288
289 7
        $authorizationRequest = $this->createAuthorizationRequest();
290 7
        $authorizationRequest->setGrantTypeId($this->getIdentifier());
291 7
        $authorizationRequest->setClient($client);
292 7
        $authorizationRequest->setRedirectUri($redirectUri);
293
294 7
        if ($stateParameter !== null) {
295 1
            $authorizationRequest->setState($stateParameter);
296
        }
297
298 7
        $authorizationRequest->setScopes($scopes);
299
300 7
        $codeChallenge = $this->getQueryStringParameter('code_challenge', $request);
301
302 7
        if ($codeChallenge !== null) {
303 2
            $codeChallengeMethod = $this->getQueryStringParameter('code_challenge_method', $request, 'plain');
304
305 2
            if ($codeChallengeMethod === null) {
306
                throw OAuthServerException::invalidRequest(
307
                    'code_challenge_method',
308
                    'Code challenge method must be provided when `code_challenge` is set.'
309
                );
310
            }
311
312 2
            if (array_key_exists($codeChallengeMethod, $this->codeChallengeVerifiers) === false) {
313 1
                throw OAuthServerException::invalidRequest(
314 1
                    'code_challenge_method',
315 1
                    'Code challenge method must be one of ' . implode(', ', array_map(
316 1
                        function ($method) {
317 1
                            return '`' . $method . '`';
318 1
                        },
319 1
                        array_keys($this->codeChallengeVerifiers)
320 1
                    ))
321 1
                );
322
            }
323
324
            // Validate code_challenge according to RFC-7636
325
            // @see: https://tools.ietf.org/html/rfc7636#section-4.2
326 1
            if (preg_match('/^[A-Za-z0-9-._~]{43,128}$/', $codeChallenge) !== 1) {
327
                throw OAuthServerException::invalidRequest(
328
                    'code_challenge',
329
                    'Code challenge must follow the specifications of RFC-7636.'
330
                );
331
            }
332
333 1
            $authorizationRequest->setCodeChallenge($codeChallenge);
334 1
            $authorizationRequest->setCodeChallengeMethod($codeChallengeMethod);
335 5
        } elseif ($this->requireCodeChallengeForPublicClients && !$client->isConfidential()) {
336 1
            throw OAuthServerException::invalidRequest('code_challenge', 'Code challenge must be provided for public clients');
337
        }
338
339 5
        return $authorizationRequest;
340
    }
341
342
    /**
343
     * {@inheritdoc}
344
     */
345 8
    public function completeAuthorizationRequest(AuthorizationRequestInterface $authorizationRequest): ResponseTypeInterface
346
    {
347 8
        if ($authorizationRequest->getUser() instanceof UserEntityInterface === false) {
348 1
            throw new LogicException('An instance of UserEntityInterface should be set on the AuthorizationRequest');
349
        }
350
351 7
        $finalRedirectUri = $authorizationRequest->getRedirectUri()
352 7
                          ?? $this->getClientRedirectUri($authorizationRequest->getClient());
353
354
        // The user approved the client, redirect them back with an auth code
355 7
        if ($authorizationRequest->isAuthorizationApproved() === true) {
356 6
            $authCode = $this->issueAuthCode(
357 6
                $this->authCodeTTL,
358 6
                $authorizationRequest->getClient(),
359 6
                $authorizationRequest->getUser()->getIdentifier(),
360 6
                $authorizationRequest->getRedirectUri(),
361 6
                $authorizationRequest->getScopes()
362 6
            );
363
364 4
            $payload = [
365 4
                'client_id'             => $authCode->getClient()->getIdentifier(),
366 4
                'redirect_uri'          => $authCode->getRedirectUri(),
367 4
                'auth_code_id'          => $authCode->getIdentifier(),
368 4
                'scopes'                => $authCode->getScopes(),
369 4
                'user_id'               => $authCode->getUserIdentifier(),
370 4
                'expire_time'           => (new DateTimeImmutable())->add($this->authCodeTTL)->getTimestamp(),
371 4
                'code_challenge'        => $authorizationRequest->getCodeChallenge(),
372 4
                'code_challenge_method' => $authorizationRequest->getCodeChallengeMethod(),
373 4
            ];
374
375 4
            $jsonPayload = json_encode($payload);
376
377 4
            if ($jsonPayload === false) {
378
                throw new LogicException('An error was encountered when JSON encoding the authorization request response');
379
            }
380
381 4
            $response = new RedirectResponse();
382 4
            $response->setRedirectUri(
383 4
                $this->makeRedirectUri(
384 4
                    $finalRedirectUri,
385 4
                    [
386 4
                        'code'  => $this->encrypt($jsonPayload),
387 4
                        'state' => $authorizationRequest->getState(),
388 4
                    ]
389 4
                )
390 4
            );
391
392 4
            return $response;
393
        }
394
395
        // The user denied the client, redirect them back with an error
396 1
        throw OAuthServerException::accessDenied(
397 1
            'The user denied the request',
398 1
            $this->makeRedirectUri(
399 1
                $finalRedirectUri,
400 1
                [
401 1
                    'state' => $authorizationRequest->getState(),
402 1
                ]
403 1
            )
404 1
        );
405
    }
406
}
407