Passed
Pull Request — master (#1110)
by Patrick
02:06
created

AuthCodeGrant::completeAuthorizationRequest()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 57
Code Lines 35

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 34
CRAP Score 4.0003

Importance

Changes 5
Bugs 0 Features 0
Metric Value
cc 4
eloc 35
c 5
b 0
f 0
nc 4
nop 1
dl 0
loc 57
ccs 34
cts 35
cp 0.9714
crap 4.0003
rs 9.36

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