Passed
Pull Request — master (#1408)
by
unknown
33:57
created

AuthCodeGrant::completeAuthorizationRequest()   B

Complexity

Conditions 5
Paths 5

Size

Total Lines 70
Code Lines 41

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 49
CRAP Score 5.0002

Importance

Changes 0
Metric Value
cc 5
eloc 41
c 0
b 0
f 0
nc 5
nop 1
dl 0
loc 70
ccs 49
cts 50
cp 0.98
crap 5.0002
rs 8.9528

How to fix   Long Method   

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 49
    public function __construct(
64
        AuthCodeRepositoryInterface $authCodeRepository,
65
        RefreshTokenRepositoryInterface $refreshTokenRepository,
66
        private DateInterval $authCodeTTL
67
    ) {
68 49
        $this->setAuthCodeRepository($authCodeRepository);
69 49
        $this->setRefreshTokenRepository($refreshTokenRepository);
70 49
        $this->refreshTokenTTL = new DateInterval('P1M');
71
72 49
        if (in_array('sha256', hash_algos(), true)) {
73 49
            $s256Verifier = new S256Verifier();
74 49
            $this->codeChallengeVerifiers[$s256Verifier->getMethod()] = $s256Verifier;
75
        }
76
77 49
        $plainVerifier = new PlainVerifier();
78 49
        $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 24
    public function respondToAccessTokenRequest(
95
        ServerRequestInterface $request,
96
        ResponseTypeInterface $responseType,
97
        DateInterval $accessTokenTTL
98
    ): ResponseTypeInterface {
99 24
        list($clientId) = $this->getClientCredentials($request);
100
101 24
        $client = $this->getClientEntityOrFail($clientId, $request);
102
103
        // Only validate the client if it is confidential
104 24
        if ($client->isConfidential()) {
105 15
            $this->validateClient($request);
106
        }
107
108 24
        $encryptedAuthCode = $this->getRequestParameter('code', $request);
109
110 23
        if ($encryptedAuthCode === null) {
111 1
            throw OAuthServerException::invalidRequest('code');
112
        }
113
114
        try {
115 22
            $authCodePayload = json_decode($this->decrypt($encryptedAuthCode));
116
117 21
            $this->validateAuthorizationCode($authCodePayload, $client, $request);
118
119 15
            $scopes = $this->scopeRepository->finalizeScopes(
120 15
                $this->validateScopes($authCodePayload->scopes),
121 15
                $this->getIdentifier(),
122 15
                $client,
123 15
                $authCodePayload->user_id,
124 15
                $authCodePayload->auth_code_id
125 15
            );
126 7
        } catch (LogicException $e) {
127 1
            throw OAuthServerException::invalidRequest('code', 'Cannot decrypt the authorization code', $e);
128
        }
129
130 15
        $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 15
        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 14
        if (isset($authCodePayload->code_challenge)) {
141 7
            $this->validateCodeChallenge($authCodePayload, $codeVerifier);
142
        }
143
144
        // Issue and persist new access token
145 9
        $accessToken = $this->issueAccessToken($accessTokenTTL, $client, $authCodePayload->user_id, $scopes);
146 9
        $this->getEmitter()->emit(new RequestAccessTokenEvent(RequestEvent::ACCESS_TOKEN_ISSUED, $request, $accessToken));
147 9
        $responseType->setAccessToken($accessToken);
148
149
        // Issue and persist new refresh token if given
150 9
        $refreshToken = $this->issueRefreshToken($accessToken);
151
152 7
        if ($refreshToken !== null) {
153 6
            $this->getEmitter()->emit(new RequestRefreshTokenEvent(RequestEvent::REFRESH_TOKEN_ISSUED, $request, $refreshToken));
154 6
            $responseType->setRefreshToken($refreshToken);
155
        }
156
157
        // Revoke used auth code
158 7
        $this->authCodeRepository->revokeAuthCode($authCodePayload->auth_code_id);
159
160 7
        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 3
                    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 21
    private function validateAuthorizationCode(
200
        stdClass $authCodePayload,
201
        ClientEntityInterface $client,
202
        ServerRequestInterface $request
203
    ): void {
204 21
        if (!property_exists($authCodePayload, 'auth_code_id')) {
205 1
            throw OAuthServerException::invalidRequest('code', 'Authorization code malformed');
206
        }
207
208 20
        if (time() > $authCodePayload->expire_time) {
209 1
            throw OAuthServerException::invalidRequest('code', 'Authorization code has expired');
210
        }
211
212 19
        if ($this->authCodeRepository->isAuthCodeRevoked($authCodePayload->auth_code_id) === true) {
213 1
            throw OAuthServerException::invalidGrant('Authorization code has been revoked');
214
        }
215
216 18
        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
221 17
        $redirectUri = $this->getRequestParameter('redirect_uri', $request);
222 17
        if ($authCodePayload->redirect_uri !== '' && $redirectUri === null) {
223 1
            throw OAuthServerException::invalidRequest('redirect_uri');
224
        }
225
226 16
        if ($authCodePayload->redirect_uri !== $redirectUri) {
227 1
            throw OAuthServerException::invalidRequest('redirect_uri', 'Invalid redirect URI');
228
        }
229
    }
230
231
    /**
232
     * Return the grant identifier that can be used in matching up requests.
233
     */
234 30
    public function getIdentifier(): string
235
    {
236 30
        return 'authorization_code';
237
    }
238
239
    /**
240
     * {@inheritdoc}
241
     */
242 2
    public function canRespondToAuthorizationRequest(ServerRequestInterface $request): bool
243
    {
244 2
        return (
245 2
            array_key_exists('response_type', $request->getQueryParams())
246 2
            && $request->getQueryParams()['response_type'] === 'code'
247 2
            && isset($request->getQueryParams()['client_id'])
248 2
        );
249
    }
250
251
    /**
252
     * {@inheritdoc}
253
     */
254 15
    public function validateAuthorizationRequest(ServerRequestInterface $request): AuthorizationRequestInterface
255
    {
256 15
        $clientId = $this->getQueryStringParameter(
257 15
            'client_id',
258 15
            $request,
259 15
            $this->getServerParameter('PHP_AUTH_USER', $request)
260 15
        );
261
262 15
        if ($clientId === null) {
263 1
            throw OAuthServerException::invalidRequest('client_id');
264
        }
265
266 14
        $client = $this->getClientEntityOrFail($clientId, $request);
267
268 13
        $redirectUri = $this->getQueryStringParameter('redirect_uri', $request);
269
270 13
        if ($redirectUri !== null) {
271 11
            $this->validateRedirectUri($redirectUri, $client, $request);
272
        } elseif (
273 2
            $client->getRedirectUri() === '' ||
274 2
            (is_array($client->getRedirectUri()) && count($client->getRedirectUri()) !== 1)
275
        ) {
276
            $this->getEmitter()->emit(new RequestEvent(RequestEvent::CLIENT_AUTHENTICATION_FAILED, $request));
277
278
            throw OAuthServerException::invalidClient($request);
279
        }
280
281 11
        $defaultClientRedirectUri = is_array($client->getRedirectUri())
282 2
            ? $client->getRedirectUri()[0]
283 9
            : $client->getRedirectUri();
284
285 11
        $scopes = $this->validateScopes(
286 11
            $this->getQueryStringParameter('scope', $request, $this->defaultScope),
287 11
            $redirectUri ?? $defaultClientRedirectUri
288 11
        );
289
290 7
        $stateParameter = $this->getQueryStringParameter('state', $request);
291
292 7
        $authorizationRequest = $this->createAuthorizationRequest();
293 7
        $authorizationRequest->setGrantTypeId($this->getIdentifier());
294 7
        $authorizationRequest->setClient($client);
295 7
        $authorizationRequest->setRedirectUri($redirectUri);
296
297 7
        if ($stateParameter !== null) {
298 1
            $authorizationRequest->setState($stateParameter);
299
        }
300
301 7
        $authorizationRequest->setScopes($scopes);
302
303 7
        $codeChallenge = $this->getQueryStringParameter('code_challenge', $request);
304
305 7
        if ($codeChallenge !== null) {
306 2
            $codeChallengeMethod = $this->getQueryStringParameter('code_challenge_method', $request, 'plain');
307
308 2
            if ($codeChallengeMethod === null) {
309
                throw OAuthServerException::invalidRequest(
310
                    'code_challenge_method',
311
                    'Code challenge method must be provided when `code_challenge` is set.'
312
                );
313
            }
314
315 2
            if (array_key_exists($codeChallengeMethod, $this->codeChallengeVerifiers) === false) {
316 1
                throw OAuthServerException::invalidRequest(
317 1
                    'code_challenge_method',
318 1
                    'Code challenge method must be one of ' . implode(', ', array_map(
319 1
                        function ($method) {
320 1
                            return '`' . $method . '`';
321 1
                        },
322 1
                        array_keys($this->codeChallengeVerifiers)
323 1
                    ))
324 1
                );
325
            }
326
327
            // Validate code_challenge according to RFC-7636
328
            // @see: https://tools.ietf.org/html/rfc7636#section-4.2
329 1
            if (preg_match('/^[A-Za-z0-9-._~]{43,128}$/', $codeChallenge) !== 1) {
330
                throw OAuthServerException::invalidRequest(
331
                    'code_challenge',
332
                    'Code challenge must follow the specifications of RFC-7636.'
333
                );
334
            }
335
336 1
            $authorizationRequest->setCodeChallenge($codeChallenge);
337 1
            $authorizationRequest->setCodeChallengeMethod($codeChallengeMethod);
338 5
        } elseif ($this->requireCodeChallengeForPublicClients && !$client->isConfidential()) {
339 1
            throw OAuthServerException::invalidRequest('code_challenge', 'Code challenge must be provided for public clients');
340
        }
341
342 5
        return $authorizationRequest;
343
    }
344
345
    /**
346
     * {@inheritdoc}
347
     */
348 8
    public function completeAuthorizationRequest(AuthorizationRequestInterface $authorizationRequest): ResponseTypeInterface
349
    {
350 8
        $finalRedirectUri = $authorizationRequest->getRedirectUri()
351 1
                          ?? $this->getClientRedirectUri($authorizationRequest);
352
353
        // The user approved the client, redirect them back with an auth code
354 7
        if ($authorizationRequest->isAuthorizationApproved() === true) {
355 7
            if ($authorizationRequest->getUser() instanceof UserEntityInterface === false) {
356
                throw new LogicException('An instance of UserEntityInterface should be set on the AuthorizationRequest');
357
            }
358 7
359 6
            $authCode = $this->issueAuthCode(
360 6
                $this->authCodeTTL,
361 6
                $authorizationRequest->getClient(),
362 6
                $authorizationRequest->getUser()->getIdentifier(),
363 6
                $authorizationRequest->getRedirectUri(),
364 6
                $authorizationRequest->getScopes()
365 6
            );
366
367 4
            $payload = [
368 4
                'client_id'             => $authCode->getClient()->getIdentifier(),
369 4
                'redirect_uri'          => $authCode->getRedirectUri(),
370 4
                'auth_code_id'          => $authCode->getIdentifier(),
371 4
                'scopes'                => $authCode->getScopes(),
372 4
                'user_id'               => $authCode->getUserIdentifier(),
373 4
                'expire_time'           => (new DateTimeImmutable())->add($this->authCodeTTL)->getTimestamp(),
374 4
                'code_challenge'        => $authorizationRequest->getCodeChallenge(),
375 4
                'code_challenge_method' => $authorizationRequest->getCodeChallengeMethod(),
376 4
            ];
377
378 4
            $jsonPayload = json_encode($payload);
379
380 4
            if ($jsonPayload === false) {
381
                throw new LogicException('An error was encountered when JSON encoding the authorization request response');
382
            }
383
384 4
            $response = new RedirectResponse();
385 4
            $response->setRedirectUri(
386 4
                $this->makeRedirectUri(
387 4
                    $finalRedirectUri,
388 4
                    [
389 4
                        'code'  => $this->encrypt($jsonPayload),
390 4
                        'state' => $authorizationRequest->getState(),
391 4
                    ]
392 4
                )
393 4
            );
394
395 4
            return $response;
396
        }
397
398
        // The user is not authenticated, redirect them back with an error
399 1
        if (is_null($authorizationRequest->getUser())) {
400 1
            throw OAuthServerException::accessDenied(
401 1
                'The user is not authenticated.',
402 1
                $this->makeRedirectUri(
403 1
                    $finalRedirectUri,
404 1
                    [
405 1
                        'state' => $authorizationRequest->getState(),
406 1
                    ]
407 1
                )
408
            );
409
        }
410
411
        // The user denied the client, redirect them back with an error
412
        throw OAuthServerException::accessDenied(
413 7
            'The user denied the request',
414
            $this->makeRedirectUri(
415 7
                $finalRedirectUri,
416 1
                [
417 7
                    'state' => $authorizationRequest->getState(),
418
                ]
419
            )
420
        );
421
    }
422
423
    /**
424
     * Get the client redirect URI if not set in the request.
425
     */
426
    private function getClientRedirectUri(AuthorizationRequestInterface $authorizationRequest): string
427
    {
428
        return is_array($authorizationRequest->getClient()->getRedirectUri())
429
                ? $authorizationRequest->getClient()->getRedirectUri()[0]
430
                : $authorizationRequest->getClient()->getRedirectUri();
431
    }
432
}
433