Completed
Pull Request — master (#997)
by TEst
02:35 queued 48s
created

AuthCodeGrant::validateAuthorizationRequest()   C

Complexity

Conditions 13
Paths 33

Size

Total Lines 86

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 50
CRAP Score 13.0096

Importance

Changes 0
Metric Value
dl 0
loc 86
ccs 50
cts 52
cp 0.9615
rs 5.5987
c 0
b 0
f 0
cc 13
nc 33
nop 1
crap 13.0096

How to fix   Long Method    Complexity   

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