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

AuthCodeGrant::respondToAccessTokenRequest()   C

Complexity

Conditions 12
Paths 40

Size

Total Lines 95
Code Lines 50

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 39
CRAP Score 12.1158

Importance

Changes 7
Bugs 0 Features 1
Metric Value
cc 12
eloc 50
nc 40
nop 3
dl 0
loc 95
ccs 39
cts 43
cp 0.907
crap 12.1158
rs 6.9666
c 7
b 0
f 1

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