Passed
Pull Request — master (#1095)
by Michał
03:13 queued 01:18
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\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