Passed
Pull Request — master (#1122)
by Sebastian
02:07
created

AuthCodeGrant::validateAuthorizationRequest()   C

Complexity

Conditions 13
Paths 42

Size

Total Lines 79
Code Lines 47

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 45
CRAP Score 13.0017

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