Passed
Pull Request — master (#1470)
by
unknown
33:53
created

AuthCodeGrant   B

Complexity

Total Complexity 48

Size/Duplication

Total Lines 348
Duplicated Lines 0 %

Test Coverage

Coverage 89.73%

Importance

Changes 3
Bugs 0 Features 0
Metric Value
eloc 157
dl 0
loc 348
ccs 166
cts 185
cp 0.8973
rs 8.5599
c 3
b 0
f 0
wmc 48

9 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 16 2
A disableRequireCodeChallengeForPublicClients() 0 3 1
A validateCodeChallenge() 0 27 6
A completeAuthorizationRequest() 0 44 3
B validateAuthorizationCode() 0 37 10
A canRespondToAuthorizationRequest() 0 6 3
B respondToAccessTokenRequest() 0 68 8
C validateAuthorizationRequest() 0 88 14
A getIdentifier() 0 3 1

How to fix   Complexity   

Complex Class

Complex classes like AuthCodeGrant often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use AuthCodeGrant, and based on these observations, apply Extract Interface, too.

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