Completed
Pull Request — master (#871)
by
unknown
01:51
created

AuthCodeGrant::completeAuthorizationRequest()   B

Complexity

Conditions 5
Paths 9

Size

Total Lines 62
Code Lines 38

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 35
CRAP Score 5.0039

Importance

Changes 4
Bugs 0 Features 0
Metric Value
dl 0
loc 62
ccs 35
cts 37
cp 0.9459
rs 8.6652
c 4
b 0
f 0
cc 5
eloc 38
nc 9
nop 1
crap 5.0039

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
 * @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 22
    public function __construct(
42
        AuthCodeRepositoryInterface $authCodeRepository,
43
        RefreshTokenRepositoryInterface $refreshTokenRepository,
44
        \DateInterval $authCodeTTL
45
    ) {
46 22
        $this->setAuthCodeRepository($authCodeRepository);
47 22
        $this->setRefreshTokenRepository($refreshTokenRepository);
48 22
        $this->authCodeTTL = $authCodeTTL;
49 22
        $this->refreshTokenTTL = new \DateInterval('P1M');
50 22
    }
51
52 8
    public function enableCodeExchangeProof()
53
    {
54 8
        $this->enableCodeExchangeProof = true;
55 8
    }
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 3
    public function respondToAccessTokenRequest(
69
        ServerRequestInterface $request,
70
        ResponseTypeInterface $responseType,
71
        \DateInterval $accessTokenTTL
72
    ) {
73
        
74
        // The redirect URI is required in this request.
75 3
            $redirectUri = $this->getRequestParameter('redirect_uri', $request, null);
76 3
            if (empty($authCodePayload->redirect_uri) === false && $redirectUri === null) {
0 ignored issues
show
Bug introduced by
The variable $authCodePayload seems only to be defined at a later point. Did you maybe move this code here without moving the variable definition?

This error can happen if you refactor code and forget to move the variable initialization.

Let’s take a look at a simple example:

function someFunction() {
    $x = 5;
    echo $x;
}

The above code is perfectly fine. Now imagine that we re-order the statements:

function someFunction() {
    echo $x;
    $x = 5;
}

In that case, $x would be read before it is initialized. This was a very basic example, however the principle is the same for the found issue.

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