Passed
Pull Request — master (#1122)
by Sebastian
17:29
created

AuthCodeGrant::validateAuthorizationRequest()   C

Complexity

Conditions 13
Paths 42

Size

Total Lines 79
Code Lines 47

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 46
CRAP Score 13.0016

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