Completed
Pull Request — master (#987)
by
unknown
04:24
created

AuthCodeGrant::validateAuthorizationRequest()   C

Complexity

Conditions 13
Paths 33

Size

Total Lines 86

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 32
CRAP Score 22.6142

Importance

Changes 0
Metric Value
dl 0
loc 86
ccs 32
cts 52
cp 0.6153
rs 5.5987
c 0
b 0
f 0
cc 13
nc 33
nop 1
crap 22.6142

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 3
    public function __construct(
48
        AuthCodeRepositoryInterface $authCodeRepository,
49
        RefreshTokenRepositoryInterface $refreshTokenRepository,
50
        DateInterval $authCodeTTL
51
    ) {
52 3
        $this->setAuthCodeRepository($authCodeRepository);
53 3
        $this->setRefreshTokenRepository($refreshTokenRepository);
54 3
        $this->authCodeTTL = $authCodeTTL;
55 3
        $this->refreshTokenTTL = new DateInterval('P1M');
56 3
    }
57
58
    public function enableCodeExchangeProof()
59
    {
60
        $this->enableCodeExchangeProof = true;
61
    }
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
    public function respondToAccessTokenRequest(
75
        ServerRequestInterface $request,
76
        ResponseTypeInterface $responseType,
77
        DateInterval $accessTokenTTL
78
    ) {
79
        // Validate request
80
        $client = $this->validateClient($request);
81
        $encryptedAuthCode = $this->getRequestParameter('code', $request, null);
82
83
        if ($encryptedAuthCode === null) {
84
            throw OAuthServerException::invalidRequest('code');
85
        }
86
87
        try {
88
            $authCodePayload = json_decode($this->decrypt($encryptedAuthCode));
89
90
            $this->validateAuthorizationCode($authCodePayload, $client, $request);
91
92
            $scopes = $this->scopeRepository->finalizeScopes(
93
                $this->validateScopes($authCodePayload->scopes),
94
                $this->getIdentifier(),
95
                $client,
96
                $authCodePayload->user_id
97
            );
98
        } catch (LogicException $e) {
99
            throw OAuthServerException::invalidRequest('code', 'Cannot decrypt the authorization code', $e);
100
        }
101
102
        // Validate code challenge
103
        if ($this->enableCodeExchangeProof === true) {
104
            $codeVerifier = $this->getRequestParameter('code_verifier', $request, null);
105
106
            if ($codeVerifier === null) {
107
                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
            if (preg_match('/^[A-Za-z0-9-._~]{43,128}$/', $codeVerifier) !== 1) {
113
                throw OAuthServerException::invalidRequest(
114
                    'code_verifier',
115
                    'Code Verifier must follow the specifications of RFC-7636.'
116
                );
117
            }
118
119
            switch ($authCodePayload->code_challenge_method) {
120
                case 'plain':
121
                    if (hash_equals($codeVerifier, $authCodePayload->code_challenge) === false) {
122
                        throw OAuthServerException::invalidGrant('Failed to verify `code_verifier`.');
123
                    }
124
125
                    break;
126
                case 'S256':
127
                    if (
128
                        hash_equals(
129
                            strtr(rtrim(base64_encode(hash('sha256', $codeVerifier, true)), '='), '+/', '-_'),
130
                            $authCodePayload->code_challenge
131
                        ) === 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
        $accessToken = $this->issueAccessToken($accessTokenTTL, $client, $authCodePayload->user_id, $scopes);
150
        $refreshToken = $this->issueRefreshToken($accessToken);
151
152
        // Send events to emitter
153
        $this->getEmitter()->emit(new RequestEvent(RequestEvent::ACCESS_TOKEN_ISSUED, $request));
154
        $this->getEmitter()->emit(new RequestEvent(RequestEvent::REFRESH_TOKEN_ISSUED, $request));
155
156
        // Inject tokens into response type
157
        $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
        $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
        $this->authCodeRepository->revokeAuthCode($authCodePayload->auth_code_id);
162
163
        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
    private function validateAuthorizationCode(
174
        $authCodePayload,
175
        ClientEntityInterface $client,
176
        ServerRequestInterface $request
177
    ) {
178
        if (time() > $authCodePayload->expire_time) {
179
            throw OAuthServerException::invalidRequest('code', 'Authorization code has expired');
180
        }
181
182
        if ($this->authCodeRepository->isAuthCodeRevoked($authCodePayload->auth_code_id) === true) {
183
            throw OAuthServerException::invalidRequest('code', 'Authorization code has been revoked');
184
        }
185
186
        if ($authCodePayload->client_id !== $client->getIdentifier()) {
187
            throw OAuthServerException::invalidRequest('code', 'Authorization code was not issued to this client');
188
        }
189
190
        // The redirect URI is required in this request
191
        $redirectUri = $this->getRequestParameter('redirect_uri', $request, null);
192
        if (empty($authCodePayload->redirect_uri) === false && $redirectUri === null) {
193
            throw OAuthServerException::invalidRequest('redirect_uri');
194
        }
195
196
        if ($authCodePayload->redirect_uri !== $redirectUri) {
197
            throw OAuthServerException::invalidRequest('redirect_uri', 'Invalid redirect URI');
198
        }
199
    }
200
201
    /**
202
     * Return the grant identifier that can be used in matching up requests.
203
     *
204
     * @return string
205
     */
206 3
    public function getIdentifier()
207
    {
208 3
        return 'authorization_code';
209
    }
210
211
    /**
212
     * {@inheritdoc}
213
     */
214 2
    public function canRespondToAuthorizationRequest(ServerRequestInterface $request)
215
    {
216
        return (
217 2
            array_key_exists('response_type', $request->getQueryParams())
218 2
            && $request->getQueryParams()['response_type'] === 'code'
219 2
            && isset($request->getQueryParams()['client_id'])
220
        );
221
    }
222
223
    /**
224
     * {@inheritdoc}
225
     */
226 2
    public function validateAuthorizationRequest(ServerRequestInterface $request)
227
    {
228 2
        $clientId = $this->getQueryStringParameter(
229 2
            'client_id',
230 2
            $request,
231 2
            $this->getServerParameter('PHP_AUTH_USER', $request)
232
        );
233
234 2
        if ($clientId === null) {
235
            throw OAuthServerException::invalidRequest('client_id');
236
        }
237
238 2
        $client = $this->clientRepository->getClientEntity(
239 2
            $clientId,
240 2
            $this->getIdentifier(),
241 2
            null,
242 2
            false
243
        );
244
245 2
        if ($client instanceof ClientEntityInterface === false) {
246
            $this->getEmitter()->emit(new RequestEvent(RequestEvent::CLIENT_AUTHENTICATION_FAILED, $request));
247
            throw OAuthServerException::invalidClient();
248
        }
249
250 2
        $redirectUri = $this->getQueryStringParameter('redirect_uri', $request);
251
252 2
        if ($redirectUri !== null) {
253
            $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 1
        $scopes = $this->validateScopes(
265 1
            $this->getQueryStringParameter('scope', $request, $this->defaultScope),
266 1
            $redirectUri
267
        );
268
269 1
        $stateParameter = $this->getQueryStringParameter('state', $request);
270
271 1
        $authorizationRequest = new AuthorizationRequest();
272 1
        $authorizationRequest->setGrantTypeId($this->getIdentifier());
273 1
        $authorizationRequest->setClient($client);
274 1
        $authorizationRequest->setRedirectUri($redirectUri);
275
276 1
        if ($stateParameter !== null) {
277
            $authorizationRequest->setState($stateParameter);
278
        }
279
280 1
        $authorizationRequest->setScopes($scopes);
281
282 1
        if ($this->enableCodeExchangeProof === true) {
283
            $codeChallenge = $this->getQueryStringParameter('code_challenge', $request);
284
            if ($codeChallenge === null) {
285
                throw OAuthServerException::invalidRequest('code_challenge');
286
            }
287
288
            $codeChallengeMethod = $this->getQueryStringParameter('code_challenge_method', $request, 'plain');
289
290
            if (\in_array($codeChallengeMethod, ['plain', 'S256'], true) === false) {
291
                throw OAuthServerException::invalidRequest(
292
                    'code_challenge_method',
293
                    '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
            if (preg_match('/^[A-Za-z0-9-._~]{43,128}$/', $codeChallenge) !== 1) {
300
                throw OAuthServerException::invalidRequest(
301
                    'code_challenged',
302
                    'Code challenge must follow the specifications of RFC-7636.'
303
                );
304
            }
305
306
            $authorizationRequest->setCodeChallenge($codeChallenge);
307
            $authorizationRequest->setCodeChallengeMethod($codeChallengeMethod);
308
        }
309
310 1
        return $authorizationRequest;
311
    }
312
313
    /**
314
     * {@inheritdoc}
315
     */
316 1
    public function completeAuthorizationRequest(AuthorizationRequest $authorizationRequest)
317
    {
318 1
        if ($authorizationRequest->getUser() instanceof UserEntityInterface === false) {
319
            throw new LogicException('An instance of UserEntityInterface should be set on the AuthorizationRequest');
320
        }
321
322 1
        $finalRedirectUri = $authorizationRequest->getRedirectUri()
323 1
                          ?? $this->getClientRedirectUri($authorizationRequest);
324
325
        // The user approved the client, redirect them back with an auth code
326 1
        if ($authorizationRequest->isAuthorizationApproved() === true) {
327 1
            $authCode = $this->issueAuthCode(
328 1
                $this->authCodeTTL,
329 1
                $authorizationRequest->getClient(),
330 1
                $authorizationRequest->getUser()->getIdentifier(),
331 1
                $authorizationRequest->getRedirectUri(),
332 1
                $authorizationRequest->getScopes()
333
            );
334
335
            $payload = [
336 1
                'client_id'             => $authCode->getClient()->getIdentifier(),
337 1
                'redirect_uri'          => $authCode->getRedirectUri(),
338 1
                'auth_code_id'          => $authCode->getIdentifier(),
339 1
                'scopes'                => $authCode->getScopes(),
340 1
                'user_id'               => $authCode->getUserIdentifier(),
341 1
                'expire_time'           => (new DateTime())->add($this->authCodeTTL)->format('U'),
342 1
                'code_challenge'        => $authorizationRequest->getCodeChallenge(),
343 1
                'code_challenge_method' => $authorizationRequest->getCodeChallengeMethod(),
344
            ];
345
346 1
            $response = new RedirectResponse();
347 1
            $response->setRedirectUri(
348 1
                $this->makeRedirectUri(
349 1
                    $finalRedirectUri,
350
                    [
351 1
                        'code'  => $this->encrypt(
352 1
                            json_encode(
353 1
                                $payload
354
                            )
355
                        ),
356 1
                        'state' => $authorizationRequest->getState(),
357
                    ]
358
                )
359
            );
360
361 1
            return $response;
362
        }
363
364
        // The user denied the client, redirect them back with an error
365
        throw OAuthServerException::accessDenied(
366
            'The user denied the request',
367
            $this->makeRedirectUri(
368
                $finalRedirectUri,
369
                [
370
                    '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 1
    private function getClientRedirectUri(AuthorizationRequest $authorizationRequest)
384
    {
385 1
        return \is_array($authorizationRequest->getClient()->getRedirectUri())
386
                ? $authorizationRequest->getClient()->getRedirectUri()[0]
387 1
                : $authorizationRequest->getClient()->getRedirectUri();
388
    }
389
}
390