Passed
Pull Request — master (#1122)
by Andrew
02:10
created

AuthCodeGrant::validateAuthorizationRequest()   C

Complexity

Conditions 13
Paths 42

Size

Total Lines 79
Code Lines 47

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 47
CRAP Score 13.0015

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