Passed
Pull Request — master (#1329)
by Andrew
49:52 queued 14:52
created

AuthCodeGrant::respondToAccessTokenRequest()   B

Complexity

Conditions 8
Paths 18

Size

Total Lines 66
Code Lines 33

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 28
CRAP Score 8.058

Importance

Changes 6
Bugs 0 Features 1
Metric Value
cc 8
eloc 33
c 6
b 0
f 1
nc 18
nop 3
dl 0
loc 66
ccs 28
cts 31
cp 0.9032
crap 8.058
rs 8.1475

How to fix   Long Method   

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\RequestAccessTokenEvent;
24
use League\OAuth2\Server\RequestEvent;
25
use League\OAuth2\Server\RequestRefreshTokenEvent;
26
use League\OAuth2\Server\RequestTypes\AuthorizationRequest;
27
use League\OAuth2\Server\ResponseTypes\RedirectResponse;
28
use League\OAuth2\Server\ResponseTypes\ResponseTypeInterface;
29
use LogicException;
30
use Psr\Http\Message\ServerRequestInterface;
31
use stdClass;
32
33
class AuthCodeGrant extends AbstractAuthorizeGrant
34
{
35
    /**
36
     * @var DateInterval
37
     */
38
    private $authCodeTTL;
39
40
    /**
41
     * @var bool
42
     */
43
    private $requireCodeChallengeForPublicClients = true;
44
45
    /**
46
     * @var CodeChallengeVerifierInterface[]
47
     */
48
    private $codeChallengeVerifiers = [];
49
50
    /**
51
     * @param AuthCodeRepositoryInterface     $authCodeRepository
52
     * @param RefreshTokenRepositoryInterface $refreshTokenRepository
53
     * @param DateInterval                    $authCodeTTL
54
     *
55
     * @throws Exception
56
     */
57 49
    public function __construct(
58
        AuthCodeRepositoryInterface $authCodeRepository,
59
        RefreshTokenRepositoryInterface $refreshTokenRepository,
60
        DateInterval $authCodeTTL
61
    ) {
62 49
        $this->setAuthCodeRepository($authCodeRepository);
63 49
        $this->setRefreshTokenRepository($refreshTokenRepository);
64 49
        $this->authCodeTTL = $authCodeTTL;
65 49
        $this->refreshTokenTTL = new DateInterval('P1M');
66
67 49
        if (\in_array('sha256', \hash_algos(), true)) {
68 49
            $s256Verifier = new S256Verifier();
69 49
            $this->codeChallengeVerifiers[$s256Verifier->getMethod()] = $s256Verifier;
70
        }
71
72 49
        $plainVerifier = new PlainVerifier();
73 49
        $this->codeChallengeVerifiers[$plainVerifier->getMethod()] = $plainVerifier;
74 49
    }
75
76
    /**
77
     * Disable the requirement for a code challenge for public clients.
78
     */
79
    public function disableRequireCodeChallengeForPublicClients()
80
    {
81
        $this->requireCodeChallengeForPublicClients = false;
82
    }
83
84
    /**
85
     * Respond to an access token request.
86
     *
87
     * @param ServerRequestInterface $request
88
     * @param ResponseTypeInterface  $responseType
89
     * @param DateInterval           $accessTokenTTL
90
     *
91
     * @throws OAuthServerException
92
     *
93
     * @return ResponseTypeInterface
94
     */
95 23
    public function respondToAccessTokenRequest(
96
        ServerRequestInterface $request,
97
        ResponseTypeInterface $responseType,
98
        DateInterval $accessTokenTTL
99
    ) {
100 23
        list($clientId) = $this->getClientCredentials($request);
101
102 23
        $client = $this->getClientEntityOrFail($clientId, $request);
103
104
        // Only validate the client if it is confidential
105 23
        if ($client->isConfidential()) {
106 14
            $this->validateClient($request);
107
        }
108
109 23
        $encryptedAuthCode = $this->getRequestParameter('code', $request, null);
110
111 23
        if (!\is_string($encryptedAuthCode)) {
112 2
            throw OAuthServerException::invalidRequest('code');
113
        }
114
115
        try {
116 21
            $authCodePayload = \json_decode($this->decrypt($encryptedAuthCode));
117
118 20
            $this->validateAuthorizationCode($authCodePayload, $client, $request);
119
120 14
            $scopes = $this->scopeRepository->finalizeScopes(
121 14
                $this->validateScopes($authCodePayload->scopes),
122 14
                $this->getIdentifier(),
123
                $client,
124 14
                $authCodePayload->user_id
125
            );
126 7
        } catch (LogicException $e) {
127 1
            throw OAuthServerException::invalidRequest('code', 'Cannot decrypt the authorization code', $e);
128
        }
129
130
        $codeVerifier = $this->getRequestParameter('code_verifier', $request, null);
131 14
132 7
        // If a code challenge isn't present but a code verifier is, reject the request to block PKCE downgrade attack
133
        if (empty($authCodePayload->code_challenge) && $codeVerifier !== null) {
134 7
            throw OAuthServerException::invalidRequest(
135 1
                'code_challenge',
136
                'code_verifier received when no code_challenge is present'
137
            );
138
        }
139
140 6
        if (!empty($authCodePayload->code_challenge)) {
141 3
            $this->validateCodeChallenge($authCodePayload, $codeVerifier);
142
        }
143 3
144
        // Issue and persist new access token
145
        $accessToken = $this->issueAccessToken($accessTokenTTL, $client, $authCodePayload->user_id, $scopes);
146
        $this->getEmitter()->emit(new RequestAccessTokenEvent(RequestEvent::ACCESS_TOKEN_ISSUED, $request, $accessToken));
147 3
        $responseType->setAccessToken($accessToken);
148 3
149 3
        // Issue and persist new refresh token if given
150
        $refreshToken = $this->issueRefreshToken($accessToken);
151 3
152 3
        if ($refreshToken !== null) {
153
            $this->getEmitter()->emit(new RequestRefreshTokenEvent(RequestEvent::REFRESH_TOKEN_ISSUED, $request, $refreshToken));
154
            $responseType->setRefreshToken($refreshToken);
155
        }
156
157
        // Revoke used auth code
158
        $this->authCodeRepository->revokeAuthCode($authCodePayload->auth_code_id);
159
160
        return $responseType;
161
    }
162
163
    private function validateCodeChallenge($authCodePayload, $codeVerifier)
164
    {
165
        if ($codeVerifier === null) {
166 9
            throw OAuthServerException::invalidRequest('code_verifier');
167 9
        }
168 9
169
        // Validate code_verifier according to RFC-7636
170
        // @see: https://tools.ietf.org/html/rfc7636#section-4.1
171 9
        if (\preg_match('/^[A-Za-z0-9-._~]{43,128}$/', $codeVerifier) !== 1) {
172
            throw OAuthServerException::invalidRequest(
173 7
                'code_verifier',
174 6
                'Code Verifier must follow the specifications of RFC-7636.'
175 6
            );
176
        }
177
178
        if (\property_exists($authCodePayload, 'code_challenge_method')) {
179 7
            if (isset($this->codeChallengeVerifiers[$authCodePayload->code_challenge_method])) {
180
                $codeChallengeVerifier = $this->codeChallengeVerifiers[$authCodePayload->code_challenge_method];
181 7
182
                if ($codeChallengeVerifier->verifyCodeChallenge($codeVerifier, $authCodePayload->code_challenge) === false) {
183
                    throw OAuthServerException::invalidGrant('Failed to verify `code_verifier`.');
184
                }
185
            } else {
186
                throw OAuthServerException::serverError(
187
                    \sprintf(
188
                        'Unsupported code challenge method `%s`',
189
                        $authCodePayload->code_challenge_method
190
                    )
191 20
                );
192
            }
193
        }
194
    }
195
196 20
    /**
197 1
     * Validate the authorization code.
198
     *
199
     * @param stdClass               $authCodePayload
200 19
     * @param ClientEntityInterface  $client
201 1
     * @param ServerRequestInterface $request
202
     */
203
    private function validateAuthorizationCode(
204 18
        $authCodePayload,
205 1
        ClientEntityInterface $client,
206
        ServerRequestInterface $request
207
    ) {
208 17
        if (!\property_exists($authCodePayload, 'auth_code_id')) {
209 1
            throw OAuthServerException::invalidRequest('code', 'Authorization code malformed');
210
        }
211
212
        if (\time() > $authCodePayload->expire_time) {
213 16
            throw OAuthServerException::invalidRequest('code', 'Authorization code has expired');
214 16
        }
215 1
216
        if ($this->authCodeRepository->isAuthCodeRevoked($authCodePayload->auth_code_id) === true) {
217
            throw OAuthServerException::invalidRequest('code', 'Authorization code has been revoked');
218 15
        }
219 1
220
        if ($authCodePayload->client_id !== $client->getIdentifier()) {
221 14
            throw OAuthServerException::invalidRequest('code', 'Authorization code was not issued to this client');
222
        }
223
224
        // The redirect URI is required in this request
225
        $redirectUri = $this->getRequestParameter('redirect_uri', $request, null);
226
        if (empty($authCodePayload->redirect_uri) === false && $redirectUri === null) {
227
            throw OAuthServerException::invalidRequest('redirect_uri');
228 33
        }
229
230 33
        if ($authCodePayload->redirect_uri !== $redirectUri) {
231
            throw OAuthServerException::invalidRequest('redirect_uri', 'Invalid redirect URI');
232
        }
233
    }
234
235
    /**
236 3
     * Return the grant identifier that can be used in matching up requests.
237
     *
238
     * @return string
239 3
     */
240 3
    public function getIdentifier()
241 3
    {
242
        return 'authorization_code';
243
    }
244
245
    /**
246
     * {@inheritdoc}
247
     */
248 17
    public function canRespondToAuthorizationRequest(ServerRequestInterface $request)
249
    {
250 17
        return (
251 17
            \array_key_exists('response_type', $request->getQueryParams())
252
            && $request->getQueryParams()['response_type'] === 'code'
253 17
            && isset($request->getQueryParams()['client_id'])
254
        );
255
    }
256 17
257 1
    /**
258
     * {@inheritdoc}
259
     */
260 16
    public function validateAuthorizationRequest(ServerRequestInterface $request)
261
    {
262 14
        $clientId = $this->getQueryStringParameter(
263
            'client_id',
264 14
            $request,
265 11
            $this->getServerParameter('PHP_AUTH_USER', $request)
266
        );
267
268
        if ($clientId === null) {
269 11
            throw OAuthServerException::invalidRequest('client_id');
270 3
        }
271 3
272 1
        $client = $this->getClientEntityOrFail($clientId, $request);
273
274 1
        $redirectUri = $this->getQueryStringParameter('redirect_uri', $request);
275
276
        if ($redirectUri !== null) {
277 11
            if (!\is_string($redirectUri)) {
0 ignored issues
show
introduced by
The condition is_string($redirectUri) is always true.
Loading history...
278 2
                throw OAuthServerException::invalidRequest('redirect_uri');
279 11
            }
280
281 11
            $this->validateRedirectUri($redirectUri, $client, $request);
282 11
        } elseif (empty($client->getRedirectUri()) ||
283 11
            (\is_array($client->getRedirectUri()) && \count($client->getRedirectUri()) !== 1)) {
284
            $this->getEmitter()->emit(new RequestEvent(RequestEvent::CLIENT_AUTHENTICATION_FAILED, $request));
285
286 10
            throw OAuthServerException::invalidClient($request);
287
        }
288 10
289 10
        $defaultClientRedirectUri = \is_array($client->getRedirectUri())
290 10
            ? $client->getRedirectUri()[0]
291 10
            : $client->getRedirectUri();
292
293 10
        $scopes = $this->validateScopes(
294
            $this->getQueryStringParameter('scope', $request, $this->defaultScope),
295
            $redirectUri ?? $defaultClientRedirectUri
296
        );
297 10
298
        $stateParameter = $this->getQueryStringParameter('state', $request);
299 10
300
        $authorizationRequest = new AuthorizationRequest();
301 10
        $authorizationRequest->setGrantTypeId($this->getIdentifier());
302 5
        $authorizationRequest->setClient($client);
303
        $authorizationRequest->setRedirectUri($redirectUri);
304 5
305 1
        if ($stateParameter !== null) {
306
            $authorizationRequest->setState($stateParameter);
307 1
        }
308
309 1
        $authorizationRequest->setScopes($scopes);
310
311 1
        $codeChallenge = $this->getQueryStringParameter('code_challenge', $request);
312
313
        if ($codeChallenge !== null) {
314
            $codeChallengeMethod = $this->getQueryStringParameter('code_challenge_method', $request, 'plain');
315
316
            if (\array_key_exists($codeChallengeMethod, $this->codeChallengeVerifiers) === false) {
317
                throw OAuthServerException::invalidRequest(
318 4
                    'code_challenge_method',
319 3
                    'Code challenge method must be one of ' . \implode(', ', \array_map(
320
                        function ($method) {
321 3
                            return '`' . $method . '`';
322
                        },
323
                        \array_keys($this->codeChallengeVerifiers)
324
                    ))
325 1
                );
326 1
            }
327 5
328 1
            // Validate code_challenge according to RFC-7636
329
            // @see: https://tools.ietf.org/html/rfc7636#section-4.2
330
            if (\preg_match('/^[A-Za-z0-9-._~]{43,128}$/', $codeChallenge) !== 1) {
331 5
                throw OAuthServerException::invalidRequest(
332
                    'code_challenge',
333
                    'Code challenge must follow the specifications of RFC-7636.'
334
                );
335
            }
336
337 7
            $authorizationRequest->setCodeChallenge($codeChallenge);
338
            $authorizationRequest->setCodeChallengeMethod($codeChallengeMethod);
339 7
        } elseif ($this->requireCodeChallengeForPublicClients && !$client->isConfidential()) {
340 1
            throw OAuthServerException::invalidRequest('code_challenge', 'Code challenge must be provided for public clients');
341
        }
342
343 6
        return $authorizationRequest;
344 6
    }
345
346
    /**
347 6
     * {@inheritdoc}
348 5
     */
349 5
    public function completeAuthorizationRequest(AuthorizationRequest $authorizationRequest)
350 5
    {
351 5
        if ($authorizationRequest->getUser() instanceof UserEntityInterface === false) {
352 5
            throw new LogicException('An instance of UserEntityInterface should be set on the AuthorizationRequest');
353 5
        }
354
355
        $finalRedirectUri = $authorizationRequest->getRedirectUri()
356
                          ?? $this->getClientRedirectUri($authorizationRequest);
357 3
358 3
        // The user approved the client, redirect them back with an auth code
359 3
        if ($authorizationRequest->isAuthorizationApproved() === true) {
360 3
            $authCode = $this->issueAuthCode(
361 3
                $this->authCodeTTL,
362 3
                $authorizationRequest->getClient(),
363 3
                $authorizationRequest->getUser()->getIdentifier(),
364 3
                $authorizationRequest->getRedirectUri(),
365
                $authorizationRequest->getScopes()
366
            );
367 3
368
            $payload = [
369 3
                'client_id'             => $authCode->getClient()->getIdentifier(),
370
                'redirect_uri'          => $authCode->getRedirectUri(),
371
                'auth_code_id'          => $authCode->getIdentifier(),
372
                'scopes'                => $authCode->getScopes(),
373 3
                'user_id'               => $authCode->getUserIdentifier(),
374 3
                'expire_time'           => (new DateTimeImmutable())->add($this->authCodeTTL)->getTimestamp(),
375 3
                'code_challenge'        => $authorizationRequest->getCodeChallenge(),
376
                'code_challenge_method' => $authorizationRequest->getCodeChallengeMethod(),
377
            ];
378 3
379 3
            $jsonPayload = \json_encode($payload);
380
381
            if ($jsonPayload === false) {
382
                throw new LogicException('An error was encountered when JSON encoding the authorization request response');
383
            }
384 3
385
            $response = new RedirectResponse();
386
            $response->setRedirectUri(
387
                $this->makeRedirectUri(
388 1
                    $finalRedirectUri,
389
                    [
390 1
                        'code'  => $this->encrypt($jsonPayload),
391
                        'state' => $authorizationRequest->getState(),
392
                    ]
393 1
                )
394
            );
395
396
            return $response;
397
        }
398
399
        // The user denied the client, redirect them back with an error
400
        throw OAuthServerException::accessDenied(
401
            'The user denied the request',
402
            $this->makeRedirectUri(
403
                $finalRedirectUri,
404
                [
405
                    'state' => $authorizationRequest->getState(),
406 6
                ]
407
            )
408 6
        );
409
    }
410 6
411
    /**
412
     * Get the client redirect URI if not set in the request.
413
     *
414
     * @param AuthorizationRequest $authorizationRequest
415
     *
416
     * @return string
417
     */
418
    private function getClientRedirectUri(AuthorizationRequest $authorizationRequest)
419
    {
420
        return \is_array($authorizationRequest->getClient()->getRedirectUri())
421
                ? $authorizationRequest->getClient()->getRedirectUri()[0]
422
                : $authorizationRequest->getClient()->getRedirectUri();
423
    }
424
}
425