Completed
Pull Request — master (#1036)
by Matt
01:47
created

AuthCodeGrant   B

Complexity

Total Complexity 43

Size/Duplication

Total Lines 364
Duplicated Lines 0 %

Coupling/Cohesion

Components 2
Dependencies 12

Test Coverage

Coverage 93.13%

Importance

Changes 0
Metric Value
wmc 43
lcom 2
cbo 12
dl 0
loc 364
ccs 149
cts 160
cp 0.9313
rs 8.96
c 0
b 0
f 0

9 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 18 2
A disableRequireCodeChallengeForPublicClients() 0 4 1
C respondToAccessTokenRequest() 0 81 10
B validateAuthorizationCode() 0 27 7
A getIdentifier() 0 4 1
A canRespondToAuthorizationRequest() 0 8 3
C validateAuthorizationRequest() 0 80 13
B completeAuthorizationRequest() 0 61 4
A getClientRedirectUri() 0 6 2

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. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

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 44
    public function __construct(
56
        AuthCodeRepositoryInterface $authCodeRepository,
57
        RefreshTokenRepositoryInterface $refreshTokenRepository,
58
        DateInterval $authCodeTTL
59
    ) {
60 44
        $this->setAuthCodeRepository($authCodeRepository);
61 44
        $this->setRefreshTokenRepository($refreshTokenRepository);
62 44
        $this->authCodeTTL = $authCodeTTL;
63 44
        $this->refreshTokenTTL = new DateInterval('P1M');
64
65 44
        if (in_array('sha256', hash_algos(), true)) {
66 44
            $s256Verifier = new S256Verifier();
67 44
            $this->codeChallengeVerifiers[$s256Verifier->getMethod()] = $s256Verifier;
68
        }
69
70 44
        $plainVerifier = new PlainVerifier();
71 44
        $this->codeChallengeVerifiers[$plainVerifier->getMethod()] = $plainVerifier;
72 44
    }
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 21
    public function respondToAccessTokenRequest(
94
        ServerRequestInterface $request,
95
        ResponseTypeInterface $responseType,
96
        DateInterval $accessTokenTTL
97
    ) {
98 21
        $client = $this->validateClient($request);
99
100 21
        $encryptedAuthCode = $this->getRequestParameter('code', $request, null);
101
102 21
        if ($encryptedAuthCode === null) {
103 1
            throw OAuthServerException::invalidRequest('code');
104
        }
105
106
        try {
107 20
            $authCodePayload = json_decode($this->decrypt($encryptedAuthCode));
108
109 19
            $this->validateAuthorizationCode($authCodePayload, $client, $request);
110
111 14
            $scopes = $this->scopeRepository->finalizeScopes(
112 14
                $this->validateScopes($authCodePayload->scopes),
113 14
                $this->getIdentifier(),
114 14
                $client,
115 14
                $authCodePayload->user_id
116
            );
117 6
        } catch (LogicException $e) {
118 1
            throw OAuthServerException::invalidRequest('code', 'Cannot decrypt the authorization code', $e);
119
        }
120
121
        // Validate code challenge
122 14
        if (!empty($authCodePayload->code_challenge)) {
123 7
            $codeVerifier = $this->getRequestParameter('code_verifier', $request, null);
124
125 7
            if ($codeVerifier === null) {
126 1
                throw OAuthServerException::invalidRequest('code_verifier');
127
            }
128
129
            // Validate code_verifier according to RFC-7636
130
            // @see: https://tools.ietf.org/html/rfc7636#section-4.1
131 6
            if (preg_match('/^[A-Za-z0-9-._~]{43,128}$/', $codeVerifier) !== 1) {
132 3
                throw OAuthServerException::invalidRequest(
133 3
                    'code_verifier',
134 3
                    'Code Verifier must follow the specifications of RFC-7636.'
135
                );
136
            }
137
138 3
            if (property_exists($authCodePayload, 'code_challenge_method')) {
139 3
                if (isset($this->codeChallengeVerifiers[$authCodePayload->code_challenge_method])) {
140 3
                    $codeChallengeVerifier = $this->codeChallengeVerifiers[$authCodePayload->code_challenge_method];
141
142 3
                    if ($codeChallengeVerifier->verifyCodeChallenge($codeVerifier, $authCodePayload->code_challenge) === false) {
143 3
                        throw OAuthServerException::invalidGrant('Failed to verify `code_verifier`.');
144
                    }
145
                } else {
146
                    throw OAuthServerException::serverError(
147
                        sprintf(
148
                            'Unsupported code challenge method `%s`',
149
                            $authCodePayload->code_challenge_method
150
                        )
151
                    );
152
                }
153
            }
154
        }
155
156
        // Issue and persist new access token
157 9
        $accessToken = $this->issueAccessToken($accessTokenTTL, $client, $authCodePayload->user_id, $scopes);
158 9
        $this->getEmitter()->emit(new RequestEvent(RequestEvent::ACCESS_TOKEN_ISSUED, $request));
159 9
        $responseType->setAccessToken($accessToken);
0 ignored issues
show
Bug introduced by
It seems like $accessToken defined by $this->issueAccessToken(...load->user_id, $scopes) on line 157 can be null; however, League\OAuth2\Server\Res...rface::setAccessToken() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

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