Passed
Pull Request — master (#1122)
by Andrew
04:11 queued 31s
created

AuthCodeGrant::respondToAccessTokenRequest()   C

Complexity

Conditions 12
Paths 40

Size

Total Lines 97
Code Lines 50

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 41
CRAP Score 12.627

Importance

Changes 7
Bugs 0 Features 1
Metric Value
cc 12
eloc 50
nc 40
nop 3
dl 0
loc 97
ccs 41
cts 49
cp 0.8367
crap 12.627
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 48
    public function __construct(
56
        AuthCodeRepositoryInterface $authCodeRepository,
57
        RefreshTokenRepositoryInterface $refreshTokenRepository,
58
        DateInterval $authCodeTTL
59
    ) {
60 48
        $this->setAuthCodeRepository($authCodeRepository);
61 48
        $this->setRefreshTokenRepository($refreshTokenRepository);
62 48
        $this->authCodeTTL = $authCodeTTL;
63 48
        $this->refreshTokenTTL = new DateInterval('P1M');
64
65 48
        if (\in_array('sha256', \hash_algos(), true)) {
66 48
            $s256Verifier = new S256Verifier();
67 48
            $this->codeChallengeVerifiers[$s256Verifier->getMethod()] = $s256Verifier;
68
        }
69
70 48
        $plainVerifier = new PlainVerifier();
71 48
        $this->codeChallengeVerifiers[$plainVerifier->getMethod()] = $plainVerifier;
72 48
    }
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 17
    public function validateAuthorizationRequest(ServerRequestInterface $request)
257
    {
258 17
        $clientId = $this->getQueryStringParameter(
259 17
            'client_id',
260 17
            $request,
261 17
            $this->getServerParameter('PHP_AUTH_USER', $request)
262
        );
263
264 17
        if ($clientId === null) {
265 1
            throw OAuthServerException::invalidRequest('client_id');
266
        }
267
268 16
        $client = $this->getClientEntityOrFail($clientId, $request);
269
270 13
        $redirectUri = $this->getQueryStringParameter('redirect_uri', $request);
271
272 13
        if ($redirectUri !== null) {
273 11
            $this->validateRedirectUri($redirectUri, $client, $request);
274 2
        } elseif (\is_array($client->getRedirectUri()) && \count($client->getRedirectUri()) !== 1) {
275
            $this->getEmitter()->emit(new RequestEvent(RequestEvent::CLIENT_AUTHENTICATION_FAILED, $request));
276
277
            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