Completed
Pull Request — master (#887)
by
unknown
02:49
created

AuthCodeGrant   B

Complexity

Total Complexity 46

Size/Duplication

Total Lines 368
Duplicated Lines 0 %

Coupling/Cohesion

Components 2
Dependencies 10

Test Coverage

Coverage 98.24%

Importance

Changes 6
Bugs 0 Features 0
Metric Value
wmc 46
lcom 2
cbo 10
dl 0
loc 368
ccs 167
cts 170
cp 0.9824
rs 8.3999
c 6
b 0
f 0

8 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 10 1
A enableCodeExchangeProof() 0 4 1
A getIdentifier() 0 4 1
A getClientIdFromRequest() 0 14 2
A canRespondToAuthorizationRequest() 0 8 3
D respondToAccessTokenRequest() 0 124 18
D validateAuthorizationRequest() 0 85 15
B completeAuthorizationRequest() 0 62 5

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 League\OAuth2\Server\Entities\ClientEntityInterface;
13
use League\OAuth2\Server\Entities\ScopeEntityInterface;
14
use League\OAuth2\Server\Entities\UserEntityInterface;
15
use League\OAuth2\Server\Exception\OAuthServerException;
16
use League\OAuth2\Server\Repositories\AuthCodeRepositoryInterface;
17
use League\OAuth2\Server\Repositories\RefreshTokenRepositoryInterface;
18
use League\OAuth2\Server\RequestEvent;
19
use League\OAuth2\Server\RequestTypes\AuthorizationRequest;
20
use League\OAuth2\Server\ResponseTypes\RedirectResponse;
21
use League\OAuth2\Server\ResponseTypes\ResponseTypeInterface;
22
use Psr\Http\Message\ServerRequestInterface;
23
24
class AuthCodeGrant extends AbstractAuthorizeGrant
25
{
26
    /**
27
     * @var \DateInterval
28
     */
29
    private $authCodeTTL;
30
31
    /**
32
     * @var bool
33
     */
34
    private $enableCodeExchangeProof = false;
35
36
    /**
37
     * @param AuthCodeRepositoryInterface     $authCodeRepository
38
     * @param RefreshTokenRepositoryInterface $refreshTokenRepository
39
     * @param \DateInterval                   $authCodeTTL
40
     */
41 41
    public function __construct(
42
        AuthCodeRepositoryInterface $authCodeRepository,
43
        RefreshTokenRepositoryInterface $refreshTokenRepository,
44
        \DateInterval $authCodeTTL
45
    ) {
46 41
        $this->setAuthCodeRepository($authCodeRepository);
47 41
        $this->setRefreshTokenRepository($refreshTokenRepository);
48 41
        $this->authCodeTTL = $authCodeTTL;
49 41
        $this->refreshTokenTTL = new \DateInterval('P1M');
50 41
    }
51
52 13
    public function enableCodeExchangeProof()
53
    {
54 13
        $this->enableCodeExchangeProof = true;
55 13
    }
56
57
    /**
58
     * Respond to an access token request.
59
     *
60
     * @param ServerRequestInterface $request
61
     * @param ResponseTypeInterface  $responseType
62
     * @param \DateInterval          $accessTokenTTL
63
     *
64
     * @throws OAuthServerException
65
     *
66
     * @return ResponseTypeInterface
67
     */
68 18
    public function respondToAccessTokenRequest(
69
        ServerRequestInterface $request,
70
        ResponseTypeInterface $responseType,
71
        \DateInterval $accessTokenTTL
72
    ) {
73
        // Validate request
74 18
        $client = $this->validateClient($request);
75 18
        $encryptedAuthCode = $this->getRequestParameter('code', $request, null);
76
77 18
        if ($encryptedAuthCode === null) {
78 1
            throw OAuthServerException::invalidRequest('code');
79
        }
80
81
        // Validate the authorization code
82
        try {
83 17
            $authCodePayload = json_decode($this->decrypt($encryptedAuthCode));
84 16
            if (time() > $authCodePayload->expire_time) {
85 1
                throw OAuthServerException::invalidRequest('code', 'Authorization code has expired');
86
            }
87
88 15
            if ($this->authCodeRepository->isAuthCodeRevoked($authCodePayload->auth_code_id) === true) {
89 1
                throw OAuthServerException::invalidRequest('code', 'Authorization code has been revoked');
90
            }
91
92 14
            if ($authCodePayload->client_id !== $client->getIdentifier()) {
93 1
                throw OAuthServerException::invalidRequest('code', 'Authorization code was not issued to this client');
94
            }
95
96
            // The redirect URI is required in this request
97 13
            $redirectUri = $this->getRequestParameter('redirect_uri', $request, null);
98 13
            if (empty($authCodePayload->redirect_uri) === false && $redirectUri === null) {
99 1
                throw OAuthServerException::invalidRequest('redirect_uri');
100
            }
101
102 12
            if ($authCodePayload->redirect_uri !== $redirectUri) {
103 1
                throw OAuthServerException::invalidRequest('redirect_uri', 'Invalid redirect URI');
104
            }
105
106 11
            $scopes = [];
107 11
            foreach ($authCodePayload->scopes as $scopeId) {
108 11
                $scope = $this->scopeRepository->getScopeEntityByIdentifier($scopeId);
109
110 11
                if ($scope instanceof ScopeEntityInterface === false) {
111
                    // @codeCoverageIgnoreStart
112
                    throw OAuthServerException::invalidScope($scopeId);
113
                    // @codeCoverageIgnoreEnd
114
                }
115
116 11
                $scopes[] = $scope;
117
            }
118
119
            // Finalize the requested scopes
120 11
            $scopes = $this->scopeRepository->finalizeScopes(
121 11
                $scopes,
122 11
                $this->getIdentifier(),
123 11
                $client,
124 11
                $authCodePayload->user_id
125
            );
126 6
        } catch (\LogicException  $e) {
127 1
            throw OAuthServerException::invalidRequest('code', 'Cannot decrypt the authorization code');
128
        }
129
130
        // Validate code challenge
131 11
        if ($this->enableCodeExchangeProof === true) {
132 7
            $codeVerifier = $this->getRequestParameter('code_verifier', $request, null);
133 7
            if ($codeVerifier === null) {
134 1
                throw OAuthServerException::invalidRequest('code_verifier');
135
            }
136
137
            // Validate code_verifier according to RFC-7636
138
            // @see: https://tools.ietf.org/html/rfc7636#section-4.1
139 6
            if (preg_match('/^[A-Za-z0-9-._~]{43,128}$/', $codeVerifier) !== 1) {
140 3
                throw OAuthServerException::invalidRequest(
141 3
                    'code_verifier',
142 3
                    'Code Verifier must follow the specifications of RFC-7636.'
143
                );
144
            }
145
146 3
            switch ($authCodePayload->code_challenge_method) {
147 3
                case 'plain':
148 2
                    if (hash_equals($codeVerifier, $authCodePayload->code_challenge) === false) {
149 1
                        throw OAuthServerException::invalidGrant('Failed to verify `code_verifier`.');
150
                    }
151
152 1
                    break;
153 1
                case 'S256':
154
                    if (
155 1
                        hash_equals(
156 1
                            strtr(rtrim(base64_encode(hash('sha256', $codeVerifier, true)), '='), '+/', '-_'),
157 1
                            $authCodePayload->code_challenge
158 1
                        ) === false
159
                    ) {
160
                        throw OAuthServerException::invalidGrant('Failed to verify `code_verifier`.');
161
                    }
162
                    // @codeCoverageIgnoreStart
163
                    break;
164
                default:
165
                    throw OAuthServerException::serverError(
166
                        sprintf(
167
                            'Unsupported code challenge method `%s`',
168
                            $authCodePayload->code_challenge_method
169
                        )
170
                    );
171
                // @codeCoverageIgnoreEnd
172
            }
173
        }
174
175
        // Issue and persist access + refresh tokens
176 6
        $accessToken = $this->issueAccessToken($accessTokenTTL, $client, $authCodePayload->user_id, $scopes);
177 6
        $refreshToken = $this->issueRefreshToken($accessToken);
178
179
        // Send events to emitter
180 4
        $this->getEmitter()->emit(new RequestEvent(RequestEvent::ACCESS_TOKEN_ISSUED, $request));
181 4
        $this->getEmitter()->emit(new RequestEvent(RequestEvent::REFRESH_TOKEN_ISSUED, $request));
182
183
        // Inject tokens into response type
184 4
        $responseType->setAccessToken($accessToken);
0 ignored issues
show
Bug introduced by
It seems like $accessToken defined by $this->issueAccessToken(...load->user_id, $scopes) on line 176 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...
185 4
        $responseType->setRefreshToken($refreshToken);
0 ignored issues
show
Bug introduced by
It seems like $refreshToken defined by $this->issueRefreshToken($accessToken) on line 177 can be null; however, League\OAuth2\Server\Res...face::setRefreshToken() 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...
186
187
        // Revoke used auth code
188 4
        $this->authCodeRepository->revokeAuthCode($authCodePayload->auth_code_id);
189
190 4
        return $responseType;
191
    }
192
193
    /**
194
     * Return the grant identifier that can be used in matching up requests.
195
     *
196
     * @return string
197
     */
198 33
    public function getIdentifier()
199
    {
200 33
        return 'authorization_code';
201
    }
202
203
    /**
204
     * Fetch the client_id parameter from the query string.
205
     *
206
     * @return string|null
207
     *
208
     * @throws OAuthServerException
209
     */
210 15
    protected function getClientIdFromRequest($request)
211
    {
212 15
        $clientId = $this->getQueryStringParameter(
213 15
            'client_id',
214 15
            $request,
215 15
            $this->getServerParameter('PHP_AUTH_USER', $request)
216
        );
217
218 15
        if (is_null($clientId)) {
219 1
            throw OAuthServerException::invalidRequest('client_id');
220
        }
221
222 14
        return $clientId;
223
    }
224
225
    /**
226
     * {@inheritdoc}
227
     */
228 4
    public function canRespondToAuthorizationRequest(ServerRequestInterface $request)
229
    {
230
        return (
231 4
            array_key_exists('response_type', $request->getQueryParams())
232 4
            && $request->getQueryParams()['response_type'] === 'code'
233 4
            && $this->getClientIdFromRequest($request) !== null
234
        );
235
    }
236
237
    /**
238
     * {@inheritdoc}
239
     */
240 13
    public function validateAuthorizationRequest(ServerRequestInterface $request)
241
    {
242 13
        $clientId = $this->getClientIdFromRequest($request);
243
244 13
        $client = $this->clientRepository->getClientEntity(
245 13
            $clientId,
246 13
            $this->getIdentifier(),
247 13
            null,
248 13
            false
249
        );
250
251 13
        if ($client instanceof ClientEntityInterface === false) {
252 1
            $this->getEmitter()->emit(new RequestEvent(RequestEvent::CLIENT_AUTHENTICATION_FAILED, $request));
253 1
            throw OAuthServerException::invalidClient();
254
        }
255
256 12
        $redirectUri = $this->getQueryStringParameter('redirect_uri', $request);
257 12
        if ($redirectUri !== null) {
258
            if (
259 10
                is_string($client->getRedirectUri())
260 10
                && (strcmp($client->getRedirectUri(), $redirectUri) !== 0)
261
            ) {
262 1
                $this->getEmitter()->emit(new RequestEvent(RequestEvent::CLIENT_AUTHENTICATION_FAILED, $request));
263 1
                throw OAuthServerException::invalidClient();
264
            } elseif (
265 9
                is_array($client->getRedirectUri())
266 9
                && in_array($redirectUri, $client->getRedirectUri(), true) === false
267
            ) {
268 1
                $this->getEmitter()->emit(new RequestEvent(RequestEvent::CLIENT_AUTHENTICATION_FAILED, $request));
269 9
                throw OAuthServerException::invalidClient();
270
            }
271 2
        } elseif (is_array($client->getRedirectUri()) && count($client->getRedirectUri()) !== 1
272 2
            || empty($client->getRedirectUri())) {
273 1
            $this->getEmitter()->emit(new RequestEvent(RequestEvent::CLIENT_AUTHENTICATION_FAILED, $request));
274 1
            throw OAuthServerException::invalidClient();
275
        } else {
276 1
            $redirectUri = is_array($client->getRedirectUri())
277
                ? $client->getRedirectUri()[0]
278 1
                : $client->getRedirectUri();
279
        }
280
281 9
        $scopes = $this->validateScopes(
282 9
            $client,
283 9
            $this->getQueryStringParameter('scope', $request, $this->defaultScope),
284 9
            $redirectUri
285
        );
286
287 9
        $stateParameter = $this->getQueryStringParameter('state', $request);
288
289 9
        $authorizationRequest = new AuthorizationRequest();
290 9
        $authorizationRequest->setGrantTypeId($this->getIdentifier());
291 9
        $authorizationRequest->setClient($client);
292 9
        $authorizationRequest->setRedirectUri($redirectUri);
293 9
        $authorizationRequest->setState($stateParameter);
294 9
        $authorizationRequest->setScopes($scopes);
295
296 9
        if ($this->enableCodeExchangeProof === true) {
297 6
            $codeChallenge = $this->getQueryStringParameter('code_challenge', $request);
298 6
            if ($codeChallenge === null) {
299 1
                throw OAuthServerException::invalidRequest('code_challenge');
300
            }
301
302 5
            $codeChallengeMethod = $this->getQueryStringParameter('code_challenge_method', $request, 'plain');
303 5
            if (in_array($codeChallengeMethod, ['plain', 'S256'], true) === false) {
304 1
                throw OAuthServerException::invalidRequest(
305 1
                    'code_challenge_method',
306 1
                    'Code challenge method must be `plain` or `S256`'
307
                );
308
            }
309
310
            // Validate code_challenge according to RFC-7636
311
            // @see: https://tools.ietf.org/html/rfc7636#section-4.2
312 4
            if (preg_match('/^[A-Za-z0-9-._~]{43,128}$/', $codeChallenge) !== 1) {
313 3
                throw OAuthServerException::invalidRequest(
314 3
                    'code_challenged',
315 3
                    'Code challenge must follow the specifications of RFC-7636.'
316
                );
317
            }
318
319 1
            $authorizationRequest->setCodeChallenge($codeChallenge);
320 1
            $authorizationRequest->setCodeChallengeMethod($codeChallengeMethod);
321
        }
322
323 4
        return $authorizationRequest;
324
    }
325
326
    /**
327
     * {@inheritdoc}
328
     */
329 7
    public function completeAuthorizationRequest(AuthorizationRequest $authorizationRequest)
330
    {
331 7
        if ($authorizationRequest->getUser() instanceof UserEntityInterface === false) {
332 1
            throw new \LogicException('An instance of UserEntityInterface should be set on the AuthorizationRequest');
333
        }
334
335 6
        $finalRedirectUri = ($authorizationRequest->getRedirectUri() === null)
336 6
            ? is_array($authorizationRequest->getClient()->getRedirectUri())
337
                ? $authorizationRequest->getClient()->getRedirectUri()[0]
338 6
                : $authorizationRequest->getClient()->getRedirectUri()
339 6
            : $authorizationRequest->getRedirectUri();
340
341
        // The user approved the client, redirect them back with an auth code
342 6
        if ($authorizationRequest->isAuthorizationApproved() === true) {
343 5
            $authCode = $this->issueAuthCode(
344 5
                $this->authCodeTTL,
345 5
                $authorizationRequest->getClient(),
346 5
                $authorizationRequest->getUser()->getIdentifier(),
347 5
                $authorizationRequest->getRedirectUri(),
348 5
                $authorizationRequest->getScopes()
349
            );
350
351
            $payload = [
352 3
                'client_id'             => $authCode->getClient()->getIdentifier(),
353 3
                'redirect_uri'          => $authCode->getRedirectUri(),
354 3
                'auth_code_id'          => $authCode->getIdentifier(),
355 3
                'scopes'                => $authCode->getScopes(),
356 3
                'user_id'               => $authCode->getUserIdentifier(),
357 3
                'expire_time'           => (new \DateTime())->add($this->authCodeTTL)->format('U'),
358 3
                'code_challenge'        => $authorizationRequest->getCodeChallenge(),
359 3
                'code_challenge_method' => $authorizationRequest->getCodeChallengeMethod(),
360
            ];
361
362 3
            $response = new RedirectResponse();
363 3
            $response->setRedirectUri(
364 3
                $this->makeRedirectUri(
365 3
                    $finalRedirectUri,
366
                    [
367 3
                        'code'  => $this->encrypt(
368 3
                            json_encode(
369 3
                                $payload
370
                            )
371
                        ),
372 3
                        'state' => $authorizationRequest->getState(),
373
                    ]
374
                )
375
            );
376
377 3
            return $response;
378
        }
379
380
        // The user denied the client, redirect them back with an error
381 1
        throw OAuthServerException::accessDenied(
382 1
            'The user denied the request',
383 1
            $this->makeRedirectUri(
384 1
                $finalRedirectUri,
385
                [
386 1
                    'state' => $authorizationRequest->getState(),
387
                ]
388
            )
389
        );
390
    }
391
}
392