Passed
Push — master ( e88f89...2c698e )
by Andrew
13:32 queued 54s
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 League\OAuth2\Server\CodeChallengeVerifiers\CodeChallengeVerifierInterface;
19
use League\OAuth2\Server\CodeChallengeVerifiers\PlainVerifier;
20
use League\OAuth2\Server\CodeChallengeVerifiers\S256Verifier;
21
use League\OAuth2\Server\Entities\ClientEntityInterface;
22
use League\OAuth2\Server\Entities\UserEntityInterface;
23
use League\OAuth2\Server\Exception\OAuthServerException;
24
use League\OAuth2\Server\Repositories\AuthCodeRepositoryInterface;
25
use League\OAuth2\Server\Repositories\RefreshTokenRepositoryInterface;
26
use League\OAuth2\Server\RequestAccessTokenEvent;
27
use League\OAuth2\Server\RequestEvent;
28
use League\OAuth2\Server\RequestRefreshTokenEvent;
29
use League\OAuth2\Server\RequestTypes\AuthorizationRequestInterface;
30
use League\OAuth2\Server\ResponseTypes\RedirectResponse;
31
use League\OAuth2\Server\ResponseTypes\ResponseTypeInterface;
32
use LogicException;
33
use Psr\Http\Message\ServerRequestInterface;
34
use stdClass;
35
36
use function array_key_exists;
37
use function array_keys;
38
use function array_map;
39
use function count;
40
use function hash_algos;
41
use function implode;
42
use function in_array;
43
use function is_array;
44
use function json_decode;
45
use function json_encode;
46
use function preg_match;
47
use function property_exists;
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 52
    public function __construct(
64
        AuthCodeRepositoryInterface $authCodeRepository,
65
        RefreshTokenRepositoryInterface $refreshTokenRepository,
66
        private DateInterval $authCodeTTL
67
    ) {
68 52
        $this->setAuthCodeRepository($authCodeRepository);
69 52
        $this->setRefreshTokenRepository($refreshTokenRepository);
70 52
        $this->refreshTokenTTL = new DateInterval('P1M');
71
72 52
        if (in_array('sha256', hash_algos(), true)) {
73 52
            $s256Verifier = new S256Verifier();
74 52
            $this->codeChallengeVerifiers[$s256Verifier->getMethod()] = $s256Verifier;
75
        }
76
77 52
        $plainVerifier = new PlainVerifier();
78 52
        $this->codeChallengeVerifiers[$plainVerifier->getMethod()] = $plainVerifier;
79
    }
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 26
    public function respondToAccessTokenRequest(
95
        ServerRequestInterface $request,
96
        ResponseTypeInterface $responseType,
97
        DateInterval $accessTokenTTL
98
    ): ResponseTypeInterface {
99 26
        list($clientId) = $this->getClientCredentials($request);
100
101 26
        $client = $this->getClientEntityOrFail($clientId, $request);
102
103
        // Only validate the client if it is confidential
104 26
        if ($client->isConfidential()) {
105 17
            $this->validateClient($request);
106
        }
107
108 26
        $encryptedAuthCode = $this->getRequestParameter('code', $request);
109
110 25
        if ($encryptedAuthCode === null) {
111 1
            throw OAuthServerException::invalidRequest('code');
112
        }
113
114
        try {
115 24
            $authCodePayload = json_decode($this->decrypt($encryptedAuthCode));
116
117 23
            $this->validateAuthorizationCode($authCodePayload, $client, $request);
118
119 16
            $scopes = $this->scopeRepository->finalizeScopes(
120 16
                $this->validateScopes($authCodePayload->scopes),
121 16
                $this->getIdentifier(),
122 16
                $client,
123 16
                $authCodePayload->user_id,
124 16
                $authCodePayload->auth_code_id
125 16
            );
126 8
        } catch (LogicException $e) {
127 1
            throw OAuthServerException::invalidRequest('code', 'Cannot decrypt the authorization code', $e);
128
        }
129
130 16
        $codeVerifier = $this->getRequestParameter('code_verifier', $request);
131
132
        // If a code challenge isn't present but a code verifier is, reject the request to block PKCE downgrade attack
133 16
        if (!isset($authCodePayload->code_challenge) && $codeVerifier !== null) {
134 1
            throw OAuthServerException::invalidRequest(
135 1
                'code_challenge',
136 1
                'code_verifier received when no code_challenge is present'
137 1
            );
138
        }
139
140 15
        if (isset($authCodePayload->code_challenge)) {
141 7
            $this->validateCodeChallenge($authCodePayload, $codeVerifier);
142
        }
143
144
        // Issue and persist new access token
145 10
        $accessToken = $this->issueAccessToken($accessTokenTTL, $client, $authCodePayload->user_id, $scopes);
146 10
        $this->getEmitter()->emit(new RequestAccessTokenEvent(RequestEvent::ACCESS_TOKEN_ISSUED, $request, $accessToken));
147 10
        $responseType->setAccessToken($accessToken);
148
149
        // Issue and persist new refresh token if given
150 10
        $refreshToken = $this->issueRefreshToken($accessToken);
151
152 8
        if ($refreshToken !== null) {
153 7
            $this->getEmitter()->emit(new RequestRefreshTokenEvent(RequestEvent::REFRESH_TOKEN_ISSUED, $request, $refreshToken));
154 7
            $responseType->setRefreshToken($refreshToken);
155
        }
156
157
        // Revoke used auth code
158 8
        $this->authCodeRepository->revokeAuthCode($authCodePayload->auth_code_id);
159
160 8
        return $responseType;
161
    }
162
163 7
    private function validateCodeChallenge(object $authCodePayload, ?string $codeVerifier): void
164
    {
165 7
        if ($codeVerifier === null) {
166 1
            throw OAuthServerException::invalidRequest('code_verifier');
167
        }
168
169
        // Validate code_verifier according to RFC-7636
170
        // @see: https://tools.ietf.org/html/rfc7636#section-4.1
171 6
        if (preg_match('/^[A-Za-z0-9-._~]{43,128}$/', $codeVerifier) !== 1) {
172 3
            throw OAuthServerException::invalidRequest(
173 3
                'code_verifier',
174 3
                'Code Verifier must follow the specifications of RFC-7636.'
175 3
            );
176
        }
177
178 3
        if (property_exists($authCodePayload, 'code_challenge_method')) {
179 3
            if (isset($this->codeChallengeVerifiers[$authCodePayload->code_challenge_method])) {
180 3
                $codeChallengeVerifier = $this->codeChallengeVerifiers[$authCodePayload->code_challenge_method];
181
182 3
                if (!isset($authCodePayload->code_challenge) || $codeChallengeVerifier->verifyCodeChallenge($codeVerifier, $authCodePayload->code_challenge) === false) {
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 23
    private function validateAuthorizationCode(
200
        stdClass $authCodePayload,
201
        ClientEntityInterface $client,
202
        ServerRequestInterface $request
203
    ): void {
204 23
        if (!property_exists($authCodePayload, 'auth_code_id')) {
205 1
            throw OAuthServerException::invalidRequest('code', 'Authorization code malformed');
206
        }
207
208 22
        if (time() > $authCodePayload->expire_time) {
209 1
            throw OAuthServerException::invalidRequest('code', 'Authorization code has expired');
210
        }
211
212 21
        if ($this->authCodeRepository->isAuthCodeRevoked($authCodePayload->auth_code_id) === true) {
213 1
            throw OAuthServerException::invalidGrant('Authorization code has been revoked');
214
        }
215
216 20
        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 19
        $redirectUri = $this->getRequestParameter('redirect_uri', $request);
223 19
        if ($authCodePayload->redirect_uri !== null && $redirectUri === null) {
224 1
            throw OAuthServerException::invalidRequest('redirect_uri');
225
        }
226
227
        // If a redirect URI has been provided ensure it matches the stored redirect URI
228 18
        if ($redirectUri !== null && $authCodePayload->redirect_uri !== $redirectUri) {
229 2
            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 32
    public function getIdentifier(): string
237
    {
238 32
        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