Passed
Push — master ( f82dfb...622eaa )
by Andrew
01:36
created

AuthCodeGrant::respondToAccessTokenRequest()   B

Complexity

Conditions 11
Paths 28

Size

Total Lines 87
Code Lines 44

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 37
CRAP Score 11.051

Importance

Changes 5
Bugs 0 Features 1
Metric Value
cc 11
eloc 44
c 5
b 0
f 1
nc 28
nop 3
dl 0
loc 87
ccs 37
cts 40
cp 0.925
crap 11.051
rs 7.3166

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
                $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
                    '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 33
    public function getIdentifier()
227
    {
228 33
        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 17
    public function validateAuthorizationRequest(ServerRequestInterface $request)
247
    {
248 17
        $clientId = $this->getQueryStringParameter(
249 17
            'client_id',
250
            $request,
251 17
            $this->getServerParameter('PHP_AUTH_USER', $request)
252
        );
253
254 17
        if ($clientId === null) {
255 1
            throw OAuthServerException::invalidRequest('client_id');
256
        }
257
258 16
        $client = $this->getClientEntityOrFail($clientId, $request);
259
260 14
        $redirectUri = $this->getQueryStringParameter('redirect_uri', $request);
261
262 14
        if ($redirectUri !== null) {
263 11
            $this->validateRedirectUri($redirectUri, $client, $request);
264 3
        } elseif (empty($client->getRedirectUri()) ||
265 3
            (\is_array($client->getRedirectUri()) && \count($client->getRedirectUri()) !== 1)) {
266 1
            $this->getEmitter()->emit(new RequestEvent(RequestEvent::CLIENT_AUTHENTICATION_FAILED, $request));
267
268 1
            throw OAuthServerException::invalidClient($request);
269
        }
270
271 11
        $defaultClientRedirectUri = \is_array($client->getRedirectUri())
272 2
            ? $client->getRedirectUri()[0]
273 11
            : $client->getRedirectUri();
274
275 11
        $scopes = $this->validateScopes(
276 11
            $this->getQueryStringParameter('scope', $request, $this->defaultScope),
277 11
            $redirectUri ?? $defaultClientRedirectUri
278
        );
279
280 10
        $stateParameter = $this->getQueryStringParameter('state', $request);
281
282 10
        $authorizationRequest = new AuthorizationRequest();
283 10
        $authorizationRequest->setGrantTypeId($this->getIdentifier());
284 10
        $authorizationRequest->setClient($client);
285 10
        $authorizationRequest->setRedirectUri($redirectUri);
286
287 10
        if ($stateParameter !== null) {
288
            $authorizationRequest->setState($stateParameter);
289
        }
290
291 10
        $authorizationRequest->setScopes($scopes);
292
293 10
        $codeChallenge = $this->getQueryStringParameter('code_challenge', $request);
294
295 10
        if ($codeChallenge !== null) {
296 5
            $codeChallengeMethod = $this->getQueryStringParameter('code_challenge_method', $request, 'plain');
297
298 5
            if (\array_key_exists($codeChallengeMethod, $this->codeChallengeVerifiers) === false) {
299 1
                throw OAuthServerException::invalidRequest(
300
                    'code_challenge_method',
301 1
                    'Code challenge method must be one of ' . \implode(', ', \array_map(
302
                        function ($method) {
303 1
                            return '`' . $method . '`';
304
                        },
305 1
                        \array_keys($this->codeChallengeVerifiers)
306
                    ))
307
                );
308
            }
309
310
            // Validate code_challenge according to RFC-7636
311
            // @see: https://tools.ietf.org/html/rfc7636#section-4.2
312 4
            if (\preg_match('/^[A-Za-z0-9-._~]{43,128}$/', $codeChallenge) !== 1) {
313 3
                throw OAuthServerException::invalidRequest(
314
                    'code_challenge',
315 3
                    'Code challenge must follow the specifications of RFC-7636.'
316
                );
317
            }
318
319 1
            $authorizationRequest->setCodeChallenge($codeChallenge);
320 1
            $authorizationRequest->setCodeChallengeMethod($codeChallengeMethod);
321 5
        } elseif ($this->requireCodeChallengeForPublicClients && !$client->isConfidential()) {
322 1
            throw OAuthServerException::invalidRequest('code_challenge', 'Code challenge must be provided for public clients');
323
        }
324
325 5
        return $authorizationRequest;
326
    }
327
328
    /**
329
     * {@inheritdoc}
330
     */
331 7
    public function completeAuthorizationRequest(AuthorizationRequest $authorizationRequest)
332
    {
333 7
        if ($authorizationRequest->getUser() instanceof UserEntityInterface === false) {
334 1
            throw new LogicException('An instance of UserEntityInterface should be set on the AuthorizationRequest');
335
        }
336
337 6
        $finalRedirectUri = $authorizationRequest->getRedirectUri()
338 6
                          ?? $this->getClientRedirectUri($authorizationRequest);
339
340
        // The user approved the client, redirect them back with an auth code
341 6
        if ($authorizationRequest->isAuthorizationApproved() === true) {
342 5
            $authCode = $this->issueAuthCode(
343 5
                $this->authCodeTTL,
344 5
                $authorizationRequest->getClient(),
345 5
                $authorizationRequest->getUser()->getIdentifier(),
346 5
                $authorizationRequest->getRedirectUri(),
347 5
                $authorizationRequest->getScopes()
348
            );
349
350
            $payload = [
351 3
                'client_id'             => $authCode->getClient()->getIdentifier(),
352 3
                'redirect_uri'          => $authCode->getRedirectUri(),
353 3
                'auth_code_id'          => $authCode->getIdentifier(),
354 3
                'scopes'                => $authCode->getScopes(),
355 3
                'user_id'               => $authCode->getUserIdentifier(),
356 3
                'expire_time'           => (new DateTimeImmutable())->add($this->authCodeTTL)->getTimestamp(),
357 3
                'code_challenge'        => $authorizationRequest->getCodeChallenge(),
358 3
                'code_challenge_method' => $authorizationRequest->getCodeChallengeMethod(),
359
            ];
360
361 3
            $jsonPayload = \json_encode($payload);
362
363 3
            if ($jsonPayload === false) {
364
                throw new LogicException('An error was encountered when JSON encoding the authorization request response');
365
            }
366
367 3
            $response = new RedirectResponse();
368 3
            $response->setRedirectUri(
369 3
                $this->makeRedirectUri(
370
                    $finalRedirectUri,
371
                    [
372 3
                        'code'  => $this->encrypt($jsonPayload),
373 3
                        'state' => $authorizationRequest->getState(),
374
                    ]
375
                )
376
            );
377
378 3
            return $response;
379
        }
380
381
        // The user denied the client, redirect them back with an error
382 1
        throw OAuthServerException::accessDenied(
383
            'The user denied the request',
384 1
            $this->makeRedirectUri(
385
                $finalRedirectUri,
386
                [
387 1
                    'state' => $authorizationRequest->getState(),
388
                ]
389
            )
390
        );
391
    }
392
393
    /**
394
     * Get the client redirect URI if not set in the request.
395
     *
396
     * @param AuthorizationRequest $authorizationRequest
397
     *
398
     * @return string
399
     */
400 6
    private function getClientRedirectUri(AuthorizationRequest $authorizationRequest)
401
    {
402 6
        return \is_array($authorizationRequest->getClient()->getRedirectUri())
403
                ? $authorizationRequest->getClient()->getRedirectUri()[0]
404 6
                : $authorizationRequest->getClient()->getRedirectUri();
405
    }
406
}
407