Passed
Push — master ( d6e0b3...82e7b7 )
by Andrew
07:05 queued 11s
created

AuthCodeGrant::completeAuthorizationRequest()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 57
Code Lines 35

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 31
CRAP Score 4.0004

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 4
eloc 35
c 1
b 0
f 0
nc 4
nop 1
dl 0
loc 57
ccs 31
cts 32
cp 0.9688
crap 4.0004
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 49
    public function __construct(
56
        AuthCodeRepositoryInterface $authCodeRepository,
57
        RefreshTokenRepositoryInterface $refreshTokenRepository,
58
        DateInterval $authCodeTTL
59
    ) {
60 49
        $this->setAuthCodeRepository($authCodeRepository);
61 49
        $this->setRefreshTokenRepository($refreshTokenRepository);
62 49
        $this->authCodeTTL = $authCodeTTL;
63 49
        $this->refreshTokenTTL = new DateInterval('P1M');
64
65 49
        if (\in_array('sha256', \hash_algos(), true)) {
66 49
            $s256Verifier = new S256Verifier();
67 49
            $this->codeChallengeVerifiers[$s256Verifier->getMethod()] = $s256Verifier;
68
        }
69
70 49
        $plainVerifier = new PlainVerifier();
71 49
        $this->codeChallengeVerifiers[$plainVerifier->getMethod()] = $plainVerifier;
72 49
    }
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 23
    public function respondToAccessTokenRequest(
94
        ServerRequestInterface $request,
95
        ResponseTypeInterface $responseType,
96
        DateInterval $accessTokenTTL
97
    ) {
98 23
        list($clientId) = $this->getClientCredentials($request);
99
100 23
        $client = $this->getClientEntityOrFail($clientId, $request);
101
102
        // Only validate the client if it is confidential
103 23
        if ($client->isConfidential()) {
104 14
            $this->validateClient($request);
105
        }
106
107 23
        $encryptedAuthCode = $this->getRequestParameter('code', $request, null);
108
109 23
        if (!\is_string($encryptedAuthCode)) {
110 2
            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
            if (!\is_string($redirectUri)) {
0 ignored issues
show
introduced by
The condition is_string($redirectUri) is always true.
Loading history...
264
                throw OAuthServerException::invalidRequest('redirect_uri');
265
            }
266
267 11
            $this->validateRedirectUri($redirectUri, $client, $request);
268 3
        } elseif (empty($client->getRedirectUri()) ||
269 3
            (\is_array($client->getRedirectUri()) && \count($client->getRedirectUri()) !== 1)) {
270 1
            $this->getEmitter()->emit(new RequestEvent(RequestEvent::CLIENT_AUTHENTICATION_FAILED, $request));
271
272 1
            throw OAuthServerException::invalidClient($request);
273
        }
274
275 11
        $defaultClientRedirectUri = \is_array($client->getRedirectUri())
276 2
            ? $client->getRedirectUri()[0]
277 11
            : $client->getRedirectUri();
278
279 11
        $scopes = $this->validateScopes(
280 11
            $this->getQueryStringParameter('scope', $request, $this->defaultScope),
281 11
            $redirectUri ?? $defaultClientRedirectUri
282
        );
283
284 10
        $stateParameter = $this->getQueryStringParameter('state', $request);
285
286 10
        $authorizationRequest = new AuthorizationRequest();
287 10
        $authorizationRequest->setGrantTypeId($this->getIdentifier());
288 10
        $authorizationRequest->setClient($client);
289 10
        $authorizationRequest->setRedirectUri($redirectUri);
290
291 10
        if ($stateParameter !== null) {
292
            $authorizationRequest->setState($stateParameter);
293
        }
294
295 10
        $authorizationRequest->setScopes($scopes);
296
297 10
        $codeChallenge = $this->getQueryStringParameter('code_challenge', $request);
298
299 10
        if ($codeChallenge !== null) {
300 5
            $codeChallengeMethod = $this->getQueryStringParameter('code_challenge_method', $request, 'plain');
301
302 5
            if (\array_key_exists($codeChallengeMethod, $this->codeChallengeVerifiers) === false) {
303 1
                throw OAuthServerException::invalidRequest(
304
                    'code_challenge_method',
305 1
                    'Code challenge method must be one of ' . \implode(', ', \array_map(
306
                        function ($method) {
307 1
                            return '`' . $method . '`';
308
                        },
309 1
                        \array_keys($this->codeChallengeVerifiers)
310
                    ))
311
                );
312
            }
313
314
            // Validate code_challenge according to RFC-7636
315
            // @see: https://tools.ietf.org/html/rfc7636#section-4.2
316 4
            if (\preg_match('/^[A-Za-z0-9-._~]{43,128}$/', $codeChallenge) !== 1) {
317 3
                throw OAuthServerException::invalidRequest(
318
                    'code_challenge',
319 3
                    'Code challenge must follow the specifications of RFC-7636.'
320
                );
321
            }
322
323 1
            $authorizationRequest->setCodeChallenge($codeChallenge);
324 1
            $authorizationRequest->setCodeChallengeMethod($codeChallengeMethod);
325 5
        } elseif ($this->requireCodeChallengeForPublicClients && !$client->isConfidential()) {
326 1
            throw OAuthServerException::invalidRequest('code_challenge', 'Code challenge must be provided for public clients');
327
        }
328
329 5
        return $authorizationRequest;
330
    }
331
332
    /**
333
     * {@inheritdoc}
334
     */
335 7
    public function completeAuthorizationRequest(AuthorizationRequest $authorizationRequest)
336
    {
337 7
        if ($authorizationRequest->getUser() instanceof UserEntityInterface === false) {
338 1
            throw new LogicException('An instance of UserEntityInterface should be set on the AuthorizationRequest');
339
        }
340
341 6
        $finalRedirectUri = $authorizationRequest->getRedirectUri()
342 6
                          ?? $this->getClientRedirectUri($authorizationRequest);
343
344
        // The user approved the client, redirect them back with an auth code
345 6
        if ($authorizationRequest->isAuthorizationApproved() === true) {
346 5
            $authCode = $this->issueAuthCode(
347 5
                $this->authCodeTTL,
348 5
                $authorizationRequest->getClient(),
349 5
                $authorizationRequest->getUser()->getIdentifier(),
350 5
                $authorizationRequest->getRedirectUri(),
351 5
                $authorizationRequest->getScopes()
352
            );
353
354
            $payload = [
355 3
                'client_id'             => $authCode->getClient()->getIdentifier(),
356 3
                'redirect_uri'          => $authCode->getRedirectUri(),
357 3
                'auth_code_id'          => $authCode->getIdentifier(),
358 3
                'scopes'                => $authCode->getScopes(),
359 3
                'user_id'               => $authCode->getUserIdentifier(),
360 3
                'expire_time'           => (new DateTimeImmutable())->add($this->authCodeTTL)->getTimestamp(),
361 3
                'code_challenge'        => $authorizationRequest->getCodeChallenge(),
362 3
                'code_challenge_method' => $authorizationRequest->getCodeChallengeMethod(),
363
            ];
364
365 3
            $jsonPayload = \json_encode($payload);
366
367 3
            if ($jsonPayload === false) {
368
                throw new LogicException('An error was encountered when JSON encoding the authorization request response');
369
            }
370
371 3
            $response = new RedirectResponse();
372 3
            $response->setRedirectUri(
373 3
                $this->makeRedirectUri(
374
                    $finalRedirectUri,
375
                    [
376 3
                        'code'  => $this->encrypt($jsonPayload),
377 3
                        'state' => $authorizationRequest->getState(),
378
                    ]
379
                )
380
            );
381
382 3
            return $response;
383
        }
384
385
        // The user denied the client, redirect them back with an error
386 1
        throw OAuthServerException::accessDenied(
387
            'The user denied the request',
388 1
            $this->makeRedirectUri(
389
                $finalRedirectUri,
390
                [
391 1
                    'state' => $authorizationRequest->getState(),
392
                ]
393
            )
394
        );
395
    }
396
397
    /**
398
     * Get the client redirect URI if not set in the request.
399
     *
400
     * @param AuthorizationRequest $authorizationRequest
401
     *
402
     * @return string
403
     */
404 6
    private function getClientRedirectUri(AuthorizationRequest $authorizationRequest)
405
    {
406 6
        return \is_array($authorizationRequest->getClient()->getRedirectUri())
407
                ? $authorizationRequest->getClient()->getRedirectUri()[0]
408 6
                : $authorizationRequest->getClient()->getRedirectUri();
409
    }
410
}
411