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

AuthCodeGrant::respondToAccessTokenRequest()   C

Complexity

Conditions 12
Paths 40

Size

Total Lines 95
Code Lines 50

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 40
CRAP Score 12.4753

Importance

Changes 7
Bugs 0 Features 1
Metric Value
cc 12
eloc 50
nc 40
nop 3
dl 0
loc 95
ccs 40
cts 47
cp 0.8511
crap 12.4753
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
                $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