Completed
Pull Request — master (#1095)
by Michał
14:52
created

AuthCodeGrant   B

Complexity

Total Complexity 45

Size/Duplication

Total Lines 373
Duplicated Lines 0 %

Test Coverage

Coverage 93.33%

Importance

Changes 22
Bugs 0 Features 1
Metric Value
eloc 162
dl 0
loc 373
ccs 154
cts 165
cp 0.9333
rs 8.8
c 22
b 0
f 1
wmc 45

9 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 17 2
A completeAuthorizationRequest() 0 57 4
A disableRequireCodeChallengeForPublicClients() 0 3 1
B validateAuthorizationCode() 0 29 8
A canRespondToAuthorizationRequest() 0 6 3
A getClientRedirectUri() 0 5 2
A getIdentifier() 0 3 1
B respondToAccessTokenRequest() 0 87 11
C validateAuthorizationRequest() 0 79 13

How to fix   Complexity   

Complex Class

Complex classes like AuthCodeGrant often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use AuthCodeGrant, and based on these observations, apply Extract Interface, too.

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