Completed
Pull Request — master (#1035)
by Matt
03:17
created

AuthCodeGrant   B

Complexity

Total Complexity 44

Size/Duplication

Total Lines 371
Duplicated Lines 0 %

Coupling/Cohesion

Components 2
Dependencies 15

Test Coverage

Coverage 93.25%

Importance

Changes 0
Metric Value
wmc 44
lcom 2
cbo 15
dl 0
loc 371
rs 8.8798
c 0
b 0
f 0
ccs 152
cts 163
cp 0.9325

9 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 18 2
A disableRequireCodeChallengeForPublicClients() 0 4 1
C respondToAccessTokenRequest() 0 88 11
B validateAuthorizationCode() 0 27 7
A getIdentifier() 0 4 1
A canRespondToAuthorizationRequest() 0 8 3
C validateAuthorizationRequest() 0 80 13
B completeAuthorizationRequest() 0 61 4
A getClientRedirectUri() 0 6 2

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. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

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
 * @author      Alex Bilbie <[email protected]>
4
 * @copyright   Copyright (c) Alex Bilbie
5
 * @license     http://mit-license.org/
6
 *
7
 * @link        https://github.com/thephpleague/oauth2-server
8
 */
9
10
namespace League\OAuth2\Server\Grant;
11
12
use DateInterval;
13
use DateTimeImmutable;
14
use Exception;
15
use League\OAuth2\Server\CodeChallengeVerifiers\CodeChallengeVerifierInterface;
16
use League\OAuth2\Server\CodeChallengeVerifiers\PlainVerifier;
17
use League\OAuth2\Server\CodeChallengeVerifiers\S256Verifier;
18
use League\OAuth2\Server\Entities\ClientEntityInterface;
19
use League\OAuth2\Server\Entities\UserEntityInterface;
20
use League\OAuth2\Server\Exception\OAuthServerException;
21
use League\OAuth2\Server\Repositories\AuthCodeRepositoryInterface;
22
use League\OAuth2\Server\Repositories\RefreshTokenRepositoryInterface;
23
use League\OAuth2\Server\RequestEvent;
24
use League\OAuth2\Server\RequestTypes\AuthorizationRequest;
25
use League\OAuth2\Server\ResponseTypes\RedirectResponse;
26
use League\OAuth2\Server\ResponseTypes\ResponseTypeInterface;
27
use LogicException;
28
use Psr\Http\Message\ServerRequestInterface;
29
use stdClass;
30
31
class AuthCodeGrant extends AbstractAuthorizeGrant
32
{
33
    /**
34
     * @var DateInterval
35
     */
36
    private $authCodeTTL;
37
38
    /**
39
     * @var bool
40
     */
41
    private $requireCodeChallengeForPublicClients = true;
42
43
    /**
44
     * @var CodeChallengeVerifierInterface[]
45
     */
46
    private $codeChallengeVerifiers = [];
47
48
    /**
49
     * @param AuthCodeRepositoryInterface     $authCodeRepository
50
     * @param RefreshTokenRepositoryInterface $refreshTokenRepository
51
     * @param DateInterval                    $authCodeTTL
52
     *
53
     * @throws Exception
54
     */
55 44
    public function __construct(
56
        AuthCodeRepositoryInterface $authCodeRepository,
57
        RefreshTokenRepositoryInterface $refreshTokenRepository,
58
        DateInterval $authCodeTTL
59
    ) {
60 44
        $this->setAuthCodeRepository($authCodeRepository);
61 44
        $this->setRefreshTokenRepository($refreshTokenRepository);
62 44
        $this->authCodeTTL = $authCodeTTL;
63 44
        $this->refreshTokenTTL = new DateInterval('P1M');
64
65 44
        if (in_array('sha256', hash_algos(), true)) {
66 44
            $s256Verifier = new S256Verifier();
67 44
            $this->codeChallengeVerifiers[$s256Verifier->getMethod()] = $s256Verifier;
68
        }
69
70 44
        $plainVerifier = new PlainVerifier();
71 44
        $this->codeChallengeVerifiers[$plainVerifier->getMethod()] = $plainVerifier;
72 44
    }
73
74
    /**
75
     * Disable the requirement for a code challenge for public clients.
76
     */
77
    public function disableRequireCodeChallengeForPublicClients()
78
    {
79
        $this->requireCodeChallengeForPublicClients = false;
80
    }
81
82
    /**
83
     * Respond to an access token request.
84
     *
85
     * @param ServerRequestInterface $request
86
     * @param ResponseTypeInterface  $responseType
87
     * @param DateInterval           $accessTokenTTL
88
     *
89
     * @throws OAuthServerException
90
     *
91
     * @return ResponseTypeInterface
92
     */
93 21
    public function respondToAccessTokenRequest(
94
        ServerRequestInterface $request,
95
        ResponseTypeInterface $responseType,
96
        DateInterval $accessTokenTTL
97
    ) {
98 21
        list($clientId) = $this->getClientCredentials($request);
99
100 21
        $client = $this->getClientEntityOrFail($clientId, $request);
101
102
        // Only validate the client if it is confidential
103 21
        if ($client->isConfidential()) {
104 14
            $this->validateClient($request);
105
        }
106
107 21
        $encryptedAuthCode = $this->getRequestParameter('code', $request, null);
108
109 21
        if ($encryptedAuthCode === null) {
110 1
            throw OAuthServerException::invalidRequest('code');
111
        }
112
113
        try {
114 20
            $authCodePayload = json_decode($this->decrypt($encryptedAuthCode));
115
116 19
            $this->validateAuthorizationCode($authCodePayload, $client, $request);
117
118 14
            $scopes = $this->scopeRepository->finalizeScopes(
119 14
                $this->validateScopes($authCodePayload->scopes),
120 14
                $this->getIdentifier(),
121 14
                $client,
122 14
                $authCodePayload->user_id
123
            );
124 6
        } catch (LogicException $e) {
125 1
            throw OAuthServerException::invalidRequest('code', 'Cannot decrypt the authorization code', $e);
126
        }
127
128
        // Validate code challenge
129 14
        if (!empty($authCodePayload->code_challenge)) {
130 7
            $codeVerifier = $this->getRequestParameter('code_verifier', $request, null);
131
132 7
            if ($codeVerifier === null) {
133 1
                throw OAuthServerException::invalidRequest('code_verifier');
134
            }
135
136
            // Validate code_verifier according to RFC-7636
137
            // @see: https://tools.ietf.org/html/rfc7636#section-4.1
138 6
            if (preg_match('/^[A-Za-z0-9-._~]{43,128}$/', $codeVerifier) !== 1) {
139 3
                throw OAuthServerException::invalidRequest(
140 3
                    'code_verifier',
141 3
                    'Code Verifier must follow the specifications of RFC-7636.'
142
                );
143
            }
144
145 3
            if (property_exists($authCodePayload, 'code_challenge_method')) {
146 3
                if (isset($this->codeChallengeVerifiers[$authCodePayload->code_challenge_method])) {
147 3
                    $codeChallengeVerifier = $this->codeChallengeVerifiers[$authCodePayload->code_challenge_method];
148
149 3
                    if ($codeChallengeVerifier->verifyCodeChallenge($codeVerifier, $authCodePayload->code_challenge) === false) {
150 3
                        throw OAuthServerException::invalidGrant('Failed to verify `code_verifier`.');
151
                    }
152
                } else {
153
                    throw OAuthServerException::serverError(
154
                        sprintf(
155
                            'Unsupported code challenge method `%s`',
156
                            $authCodePayload->code_challenge_method
157
                        )
158
                    );
159
                }
160
            }
161
        }
162
163
        // Issue and persist new access token
164 9
        $accessToken = $this->issueAccessToken($accessTokenTTL, $client, $authCodePayload->user_id, $scopes);
165 9
        $this->getEmitter()->emit(new RequestEvent(RequestEvent::ACCESS_TOKEN_ISSUED, $request));
166 9
        $responseType->setAccessToken($accessToken);
0 ignored issues
show
Bug introduced by
It seems like $accessToken defined by $this->issueAccessToken(...load->user_id, $scopes) on line 164 can be null; however, League\OAuth2\Server\Res...rface::setAccessToken() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
167
168
        // Issue and persist new refresh token if given
169 9
        $refreshToken = $this->issueRefreshToken($accessToken);
0 ignored issues
show
Bug introduced by
It seems like $accessToken defined by $this->issueAccessToken(...load->user_id, $scopes) on line 164 can be null; however, League\OAuth2\Server\Gra...nt::issueRefreshToken() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

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