AuthCodeGrant::respondToAccessTokenRequest()   B
last analyzed

Complexity

Conditions 8
Paths 12

Size

Total Lines 62
Code Lines 33

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 31
CRAP Score 8.0955

Importance

Changes 3
Bugs 0 Features 0
Metric Value
cc 8
eloc 33
nc 12
nop 3
dl 0
loc 62
ccs 31
cts 35
cp 0.8857
crap 8.0955
rs 8.1475
c 3
b 0
f 0

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 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