Passed
Pull Request — master (#1122)
by Andrew
30:47
created

AuthCodeGrant::validateAuthorizationRequest()   C

Complexity

Conditions 13
Paths 42

Size

Total Lines 79
Code Lines 47

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 44
CRAP Score 13.0018

Importance

Changes 9
Bugs 0 Features 1
Metric Value
cc 13
eloc 47
c 9
b 0
f 1
nc 42
nop 1
dl 0
loc 79
ccs 44
cts 45
cp 0.9778
crap 13.0018
rs 6.6166

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 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 47
    public function __construct(
56
        AuthCodeRepositoryInterface $authCodeRepository,
57
        RefreshTokenRepositoryInterface $refreshTokenRepository,
58
        DateInterval $authCodeTTL
59
    ) {
60 47
        $this->setAuthCodeRepository($authCodeRepository);
61 47
        $this->setRefreshTokenRepository($refreshTokenRepository);
62 47
        $this->authCodeTTL = $authCodeTTL;
63 47
        $this->refreshTokenTTL = new DateInterval('P1M');
64
65 47
        if (\in_array('sha256', \hash_algos(), true)) {
66 47
            $s256Verifier = new S256Verifier();
67 47
            $this->codeChallengeVerifiers[$s256Verifier->getMethod()] = $s256Verifier;
68
        }
69
70 47
        $plainVerifier = new PlainVerifier();
71 47
        $this->codeChallengeVerifiers[$plainVerifier->getMethod()] = $plainVerifier;
72 47
    }
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 22
    public function respondToAccessTokenRequest(
94
        ServerRequestInterface $request,
95
        ResponseTypeInterface $responseType,
96
        DateInterval $accessTokenTTL
97
    ) {
98 22
        list($clientId) = $this->getClientCredentials($request);
99
100 22
        $client = $this->getClientEntityOrFail($clientId, $request);
101
102
        // Only validate the client if it is confidential
103 22
        if ($client->isConfidential()) {
104 14
            $this->validateClient($request);
105
        }
106
107 22
        $encryptedAuthCode = $this->getRequestParameter('code', $request, null);
108
109 22
        if ($encryptedAuthCode === null) {
110 1
            throw OAuthServerException::invalidRequest('code');
111
        }
112
113
        try {
114 21
            $authCodePayload = \json_decode($this->decrypt($encryptedAuthCode));
115
116 20
            $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 7
        } 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
        $privateClaims = [];
163
        if ($this->claimRepository) {
164 9
            $privateClaims = $this->claimRepository->getClaims(
165 9
                $this->getIdentifier(),
166 9
                $client,
167
                $authCodePayload->user_id
168
            );
169 9
        }
170
171 7
        // Issue and persist new access token
172 6
        $accessToken = $this->issueAccessToken($accessTokenTTL, $client, $authCodePayload->user_id, $scopes, $privateClaims);
173 6
        $this->getEmitter()->emit(new RequestEvent(RequestEvent::ACCESS_TOKEN_ISSUED, $request));
174
        $responseType->setAccessToken($accessToken);
175
176
        // Issue and persist new refresh token if given
177 7
        $refreshToken = $this->issueRefreshToken($accessToken);
178
179 7
        if ($refreshToken !== null) {
180
            $this->getEmitter()->emit(new RequestEvent(RequestEvent::REFRESH_TOKEN_ISSUED, $request));
181
            $responseType->setRefreshToken($refreshToken);
182
        }
183
184
        // Revoke used auth code
185
        $this->authCodeRepository->revokeAuthCode($authCodePayload->auth_code_id);
186
187
        return $responseType;
188
    }
189 20
190
    /**
191
     * Validate the authorization code.
192
     *
193
     * @param stdClass               $authCodePayload
194 20
     * @param ClientEntityInterface  $client
195 1
     * @param ServerRequestInterface $request
196
     */
197
    private function validateAuthorizationCode(
198 19
        $authCodePayload,
199 1
        ClientEntityInterface $client,
200
        ServerRequestInterface $request
201
    ) {
202 18
        if (!\property_exists($authCodePayload, 'auth_code_id')) {
203 1
            throw OAuthServerException::invalidRequest('code', 'Authorization code malformed');
204
        }
205
206 17
        if (\time() > $authCodePayload->expire_time) {
207 1
            throw OAuthServerException::invalidRequest('code', 'Authorization code has expired');
208
        }
209
210
        if ($this->authCodeRepository->isAuthCodeRevoked($authCodePayload->auth_code_id) === true) {
211 16
            throw OAuthServerException::invalidRequest('code', 'Authorization code has been revoked');
212 16
        }
213 1
214
        if ($authCodePayload->client_id !== $client->getIdentifier()) {
215
            throw OAuthServerException::invalidRequest('code', 'Authorization code was not issued to this client');
216 15
        }
217 1
218
        // The redirect URI is required in this request
219 14
        $redirectUri = $this->getRequestParameter('redirect_uri', $request, null);
220
        if (empty($authCodePayload->redirect_uri) === false && $redirectUri === null) {
221
            throw OAuthServerException::invalidRequest('redirect_uri');
222
        }
223
224
        if ($authCodePayload->redirect_uri !== $redirectUri) {
225
            throw OAuthServerException::invalidRequest('redirect_uri', 'Invalid redirect URI');
226 33
        }
227
    }
228 33
229
    /**
230
     * Return the grant identifier that can be used in matching up requests.
231
     *
232
     * @return string
233
     */
234 3
    public function getIdentifier()
235
    {
236
        return 'authorization_code';
237 3
    }
238 3
239 3
    /**
240
     * {@inheritdoc}
241
     */
242
    public function canRespondToAuthorizationRequest(ServerRequestInterface $request)
243
    {
244
        return (
245
            \array_key_exists('response_type', $request->getQueryParams())
246 16
            && $request->getQueryParams()['response_type'] === 'code'
247
            && isset($request->getQueryParams()['client_id'])
248 16
        );
249 16
    }
250 16
251 16
    /**
252
     * {@inheritdoc}
253
     */
254 16
    public function validateAuthorizationRequest(ServerRequestInterface $request)
255 1
    {
256
        $clientId = $this->getQueryStringParameter(
257
            'client_id',
258 15
            $request,
259
            $this->getServerParameter('PHP_AUTH_USER', $request)
260 14
        );
261
262 14
        if ($clientId === null) {
263 11
            throw OAuthServerException::invalidRequest('client_id');
264 3
        }
265 3
266 1
        $client = $this->getClientEntityOrFail($clientId, $request);
267 1
268
        $redirectUri = $this->getQueryStringParameter('redirect_uri', $request);
269
270 11
        if ($redirectUri !== null) {
271 2
            $this->validateRedirectUri($redirectUri, $client, $request);
272 11
        } elseif (empty($client->getRedirectUri()) ||
273
            (\is_array($client->getRedirectUri()) && \count($client->getRedirectUri()) !== 1)) {
274 11
            $this->getEmitter()->emit(new RequestEvent(RequestEvent::CLIENT_AUTHENTICATION_FAILED, $request));
275 11
            throw OAuthServerException::invalidClient($request);
276 11
        }
277
278
        $defaultClientRedirectUri = \is_array($client->getRedirectUri())
279 10
            ? $client->getRedirectUri()[0]
280
            : $client->getRedirectUri();
281 10
282 10
        $scopes = $this->validateScopes(
283 10
            $this->getQueryStringParameter('scope', $request, $this->defaultScope),
284 10
            $redirectUri ?? $defaultClientRedirectUri
285
        );
286 10
287
        $stateParameter = $this->getQueryStringParameter('state', $request);
288
289
        $authorizationRequest = new AuthorizationRequest();
290 10
        $authorizationRequest->setGrantTypeId($this->getIdentifier());
291
        $authorizationRequest->setClient($client);
292 10
        $authorizationRequest->setRedirectUri($redirectUri);
293
294 10
        if ($stateParameter !== null) {
295 5
            $authorizationRequest->setState($stateParameter);
296
        }
297 5
298 1
        $authorizationRequest->setScopes($scopes);
299 1
300 1
        $codeChallenge = $this->getQueryStringParameter('code_challenge', $request);
301
302 1
        if ($codeChallenge !== null) {
303 1
            $codeChallengeMethod = $this->getQueryStringParameter('code_challenge_method', $request, 'plain');
304 1
305
            if (\array_key_exists($codeChallengeMethod, $this->codeChallengeVerifiers) === false) {
306
                throw OAuthServerException::invalidRequest(
307
                    'code_challenge_method',
308
                    'Code challenge method must be one of ' . \implode(', ', \array_map(
309
                        function ($method) {
310
                            return '`' . $method . '`';
311 4
                        },
312 3
                        \array_keys($this->codeChallengeVerifiers)
313 3
                    ))
314 3
                );
315
            }
316
317
            // Validate code_challenge according to RFC-7636
318 1
            // @see: https://tools.ietf.org/html/rfc7636#section-4.2
319 1
            if (\preg_match('/^[A-Za-z0-9-._~]{43,128}$/', $codeChallenge) !== 1) {
320 5
                throw OAuthServerException::invalidRequest(
321 1
                    'code_challenge',
322
                    'Code challenge must follow the specifications of RFC-7636.'
323
                );
324 5
            }
325
326
            $authorizationRequest->setCodeChallenge($codeChallenge);
327
            $authorizationRequest->setCodeChallengeMethod($codeChallengeMethod);
328
        } elseif ($this->requireCodeChallengeForPublicClients && !$client->isConfidential()) {
329
            throw OAuthServerException::invalidRequest('code_challenge', 'Code challenge must be provided for public clients');
330 7
        }
331
332 7
        return $authorizationRequest;
333 1
    }
334
335
    /**
336 6
     * {@inheritdoc}
337 6
     */
338
    public function completeAuthorizationRequest(AuthorizationRequest $authorizationRequest)
339
    {
340 6
        if ($authorizationRequest->getUser() instanceof UserEntityInterface === false) {
341 5
            throw new LogicException('An instance of UserEntityInterface should be set on the AuthorizationRequest');
342 5
        }
343 5
344 5
        $finalRedirectUri = $authorizationRequest->getRedirectUri()
345 5
                          ?? $this->getClientRedirectUri($authorizationRequest);
346 5
347
        // The user approved the client, redirect them back with an auth code
348
        if ($authorizationRequest->isAuthorizationApproved() === true) {
349
            $authCode = $this->issueAuthCode(
350 3
                $this->authCodeTTL,
351 3
                $authorizationRequest->getClient(),
352 3
                $authorizationRequest->getUser()->getIdentifier(),
353 3
                $authorizationRequest->getRedirectUri(),
354 3
                $authorizationRequest->getScopes()
355 3
            );
356 3
357 3
            $payload = [
358
                'client_id'             => $authCode->getClient()->getIdentifier(),
359
                'redirect_uri'          => $authCode->getRedirectUri(),
360 3
                'auth_code_id'          => $authCode->getIdentifier(),
361
                'scopes'                => $authCode->getScopes(),
362 3
                'user_id'               => $authCode->getUserIdentifier(),
363
                'expire_time'           => (new DateTimeImmutable())->add($this->authCodeTTL)->getTimestamp(),
364
                'code_challenge'        => $authorizationRequest->getCodeChallenge(),
365
                'code_challenge_method' => $authorizationRequest->getCodeChallengeMethod(),
366 3
            ];
367 3
368 3
            $jsonPayload = \json_encode($payload);
369 3
370
            if ($jsonPayload === false) {
371 3
                throw new LogicException('An error was encountered when JSON encoding the authorization request response');
372 3
            }
373
374
            $response = new RedirectResponse();
375
            $response->setRedirectUri(
376
                $this->makeRedirectUri(
377 3
                    $finalRedirectUri,
378
                    [
379
                        'code'  => $this->encrypt($jsonPayload),
380
                        'state' => $authorizationRequest->getState(),
381 1
                    ]
382 1
                )
383 1
            );
384 1
385
            return $response;
386 1
        }
387
388
        // The user denied the client, redirect them back with an error
389
        throw OAuthServerException::accessDenied(
390
            'The user denied the request',
391
            $this->makeRedirectUri(
392
                $finalRedirectUri,
393
                [
394
                    'state' => $authorizationRequest->getState(),
395
                ]
396
            )
397
        );
398
    }
399 6
400
    /**
401 6
     * Get the client redirect URI if not set in the request.
402
     *
403 6
     * @param AuthorizationRequest $authorizationRequest
404
     *
405
     * @return string
406
     */
407
    private function getClientRedirectUri(AuthorizationRequest $authorizationRequest)
408
    {
409
        return \is_array($authorizationRequest->getClient()->getRedirectUri())
410
                ? $authorizationRequest->getClient()->getRedirectUri()[0]
411
                : $authorizationRequest->getClient()->getRedirectUri();
412
    }
413
}
414