Completed
Pull Request — master (#817)
by
unknown
01:49
created

AuthCodeGrant::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 10
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 1

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 10
ccs 6
cts 6
cp 1
rs 9.4285
cc 1
eloc 8
nc 1
nop 3
crap 1
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 League\OAuth2\Server\Entities\ClientEntityInterface;
13
use League\OAuth2\Server\Entities\ScopeEntityInterface;
14
use League\OAuth2\Server\Entities\UserEntityInterface;
15
use League\OAuth2\Server\Exception\OAuthServerException;
16
use League\OAuth2\Server\Repositories\AuthCodeRepositoryInterface;
17
use League\OAuth2\Server\Repositories\RefreshTokenRepositoryInterface;
18
use League\OAuth2\Server\RequestEvent;
19
use League\OAuth2\Server\RequestTypes\AuthorizationRequest;
20
use League\OAuth2\Server\ResponseTypes\RedirectResponse;
21
use League\OAuth2\Server\ResponseTypes\ResponseTypeInterface;
22
use Psr\Http\Message\ServerRequestInterface;
23
24
class AuthCodeGrant extends AbstractAuthorizeGrant
25
{
26
    /**
27
     * @var \DateInterval
28
     */
29
    private $authCodeTTL;
30
31
    /**
32
     * @var bool
33
     */
34
    private $enableCodeExchangeProof = false;
35
36
    /**
37
     * @var bool
38
     */
39
    private $enablePkceFallback = false;
40
41
    /**
42
     * @param AuthCodeRepositoryInterface     $authCodeRepository
43
     * @param RefreshTokenRepositoryInterface $refreshTokenRepository
44
     * @param \DateInterval                   $authCodeTTL
45
     */
46 43
    public function __construct(
47
        AuthCodeRepositoryInterface $authCodeRepository,
48
        RefreshTokenRepositoryInterface $refreshTokenRepository,
49
        \DateInterval $authCodeTTL
50
    ) {
51 43
        $this->setAuthCodeRepository($authCodeRepository);
52 43
        $this->setRefreshTokenRepository($refreshTokenRepository);
53 43
        $this->authCodeTTL = $authCodeTTL;
54 43
        $this->refreshTokenTTL = new \DateInterval('P1M');
55 43
    }
56
57 13
    public function enableCodeExchangeProof()
58
    {
59 13
        $this->enableCodeExchangeProof = true;
60 13
    }
61
62
    /**
63
     *
64
     */
65 2
    public function enablePkceFallback()
66
    {
67 2
        $this->enablePkceFallback = true;
68 2
    }
69
70
    /**
71
     * Respond to an access token request.
72
     *
73
     * @param ServerRequestInterface $request
74
     * @param ResponseTypeInterface  $responseType
75
     * @param \DateInterval          $accessTokenTTL
76
     *
77
     * @throws OAuthServerException
78
     *
79
     * @return ResponseTypeInterface
80
     */
81 19
    public function respondToAccessTokenRequest(
82
        ServerRequestInterface $request,
83
        ResponseTypeInterface $responseType,
84
        \DateInterval $accessTokenTTL
85
    ) {
86
        // Validate request
87 19
        $client = $this->validateClient($request);
88 19
        $encryptedAuthCode = $this->getRequestParameter('code', $request, null);
89
90 19
        if ($encryptedAuthCode === null) {
91 1
            throw OAuthServerException::invalidRequest('code');
92
        }
93
94
        // Validate the authorization code
95
        try {
96 18
            $authCodePayload = json_decode($this->decrypt($encryptedAuthCode));
97 17
            if (time() > $authCodePayload->expire_time) {
98 1
                throw OAuthServerException::invalidRequest('code', 'Authorization code has expired');
99
            }
100
101 16
            if ($this->authCodeRepository->isAuthCodeRevoked($authCodePayload->auth_code_id) === true) {
102 1
                throw OAuthServerException::invalidRequest('code', 'Authorization code has been revoked');
103
            }
104
105 15
            if ($authCodePayload->client_id !== $client->getIdentifier()) {
106 1
                throw OAuthServerException::invalidRequest('code', 'Authorization code was not issued to this client');
107
            }
108
109
            // The redirect URI is required in this request
110 14
            $redirectUri = $this->getRequestParameter('redirect_uri', $request, null);
111 14
            if (empty($authCodePayload->redirect_uri) === false && $redirectUri === null) {
112 1
                throw OAuthServerException::invalidRequest('redirect_uri');
113
            }
114
115 13
            if ($authCodePayload->redirect_uri !== $redirectUri) {
116 1
                throw OAuthServerException::invalidRequest('redirect_uri', 'Invalid redirect URI');
117
            }
118
119 12
            $scopes = [];
120 12
            foreach ($authCodePayload->scopes as $scopeId) {
121 12
                $scope = $this->scopeRepository->getScopeEntityByIdentifier($scopeId);
122
123 12
                if ($scope instanceof ScopeEntityInterface === false) {
124
                    // @codeCoverageIgnoreStart
125
                    throw OAuthServerException::invalidScope($scopeId);
126
                    // @codeCoverageIgnoreEnd
127
                }
128
129 12
                $scopes[] = $scope;
130
            }
131
132
            // Finalize the requested scopes
133 12
            $scopes = $this->scopeRepository->finalizeScopes(
134 12
                $scopes,
135 12
                $this->getIdentifier(),
136 12
                $client,
137 12
                $authCodePayload->user_id
138
            );
139 6
        } catch (\LogicException  $e) {
140 1
            throw OAuthServerException::invalidRequest('code', 'Cannot decrypt the authorization code');
141
        }
142
143
        // Validate code challenge
144 12
        if ($this->shouldValidatePKCE(isset($authCodePayload->code_challenge) ? $authCodePayload->code_challenge : null)) {
145 8
            $codeVerifier = $this->getRequestParameter('code_verifier', $request, null);
146 8
            if ($codeVerifier === null) {
147 1
                throw OAuthServerException::invalidRequest('code_verifier');
148
            }
149
150
            // Validate code_verifier according to RFC-7636
151
            // @see: https://tools.ietf.org/html/rfc7636#section-4.1
152 7
            if (preg_match('/^[A-Za-z0-9-._~]{43,128}$/', $codeVerifier) !== 1) {
153 3
                throw OAuthServerException::invalidRequest(
154 3
                    'code_verifier',
155 3
                    'Code Verifier must follow the specifications of RFC-7636.'
156
                );
157
            }
158
159 4
            switch ($authCodePayload->code_challenge_method) {
160 4
                case 'plain':
161 3
                    if (hash_equals($codeVerifier, $authCodePayload->code_challenge) === false) {
162 2
                        throw OAuthServerException::invalidGrant('Failed to verify `code_verifier`.');
163
                    }
164
165 1
                    break;
166 1
                case 'S256':
167
                    if (
168 1
                        hash_equals(
169 1
                            strtr(rtrim(base64_encode(hash('sha256', $codeVerifier, true)), '='), '+/', '-_'),
170 1
                            $authCodePayload->code_challenge
171 1
                        ) === false
172
                    ) {
173
                        throw OAuthServerException::invalidGrant('Failed to verify `code_verifier`.');
174
                    }
175
                    // @codeCoverageIgnoreStart
176
                    break;
177
                default:
178
                    throw OAuthServerException::serverError(
179
                        sprintf(
180
                            'Unsupported code challenge method `%s`',
181
                            $authCodePayload->code_challenge_method
182
                        )
183
                    );
184
                // @codeCoverageIgnoreEnd
185
            }
186
        }
187
188
        // Issue and persist access + refresh tokens
189 6
        $accessToken = $this->issueAccessToken($accessTokenTTL, $client, $authCodePayload->user_id, $scopes);
190 6
        $refreshToken = $this->issueRefreshToken($accessToken);
191
192
        // Send events to emitter
193 4
        $this->getEmitter()->emit(new RequestEvent(RequestEvent::ACCESS_TOKEN_ISSUED, $request));
194 4
        $this->getEmitter()->emit(new RequestEvent(RequestEvent::REFRESH_TOKEN_ISSUED, $request));
195
196
        // Inject tokens into response type
197 4
        $responseType->setAccessToken($accessToken);
0 ignored issues
show
Bug introduced by
It seems like $accessToken defined by $this->issueAccessToken(...load->user_id, $scopes) on line 189 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...
198 4
        $responseType->setRefreshToken($refreshToken);
0 ignored issues
show
Bug introduced by
It seems like $refreshToken defined by $this->issueRefreshToken($accessToken) on line 190 can be null; however, League\OAuth2\Server\Res...face::setRefreshToken() 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...
199
200
        // Revoke used auth code
201 4
        $this->authCodeRepository->revokeAuthCode($authCodePayload->auth_code_id);
202
203 4
        return $responseType;
204
    }
205
206
    /**
207
     * Return the grant identifier that can be used in matching up requests.
208
     *
209
     * @return string
210
     */
211 35
    public function getIdentifier()
212
    {
213 35
        return 'authorization_code';
214
    }
215
216
    /**
217
     * Fetch the client_id parameter from the query string.
218
     *
219
     * @return string|null
220
     * @throws OAuthServerException
221
     */
222 16
    protected function getClientIdFromRequest($request)
223
    {
224 16
        $clientId = $this->getQueryStringParameter(
225 16
            'client_id',
226 16
            $request,
227 16
            $this->getServerParameter('PHP_AUTH_USER', $request)
228
        );
229
230 16
        if (is_null($clientId)) {
231 1
            throw OAuthServerException::invalidRequest('client_id');
232
        }
233
234 15
        return $clientId;
235
    }
236
237
    /**
238
     * {@inheritdoc}
239
     */
240 4
    public function canRespondToAuthorizationRequest(ServerRequestInterface $request)
241
    {
242
        return (
243 4
            array_key_exists('response_type', $request->getQueryParams())
244 4
            && $request->getQueryParams()['response_type'] === 'code'
245 4
            && $this->getClientIdFromRequest($request) !== null
246
        );
247
    }
248
249
    /**
250
     * {@inheritdoc}
251
     */
252 14
    public function validateAuthorizationRequest(ServerRequestInterface $request)
253
    {
254 14
        $clientId = $this->getClientIdFromRequest($request);
255
256 14
        $client = $this->clientRepository->getClientEntity(
257 14
            $clientId,
258 14
            $this->getIdentifier(),
259 14
            null,
260 14
            false
261
        );
262
263 14
        if ($client instanceof ClientEntityInterface === false) {
264 1
            $this->getEmitter()->emit(new RequestEvent(RequestEvent::CLIENT_AUTHENTICATION_FAILED, $request));
265 1
            throw OAuthServerException::invalidClient();
266
        }
267
268 13
        $redirectUri = $this->getQueryStringParameter('redirect_uri', $request);
269 13
        if ($redirectUri !== null) {
270
            if (
271 11
                is_string($client->getRedirectUri())
272 11
                && (strcmp($client->getRedirectUri(), $redirectUri) !== 0)
273
            ) {
274 1
                $this->getEmitter()->emit(new RequestEvent(RequestEvent::CLIENT_AUTHENTICATION_FAILED, $request));
275 1
                throw OAuthServerException::invalidClient();
276
            } elseif (
277 10
                is_array($client->getRedirectUri())
278 10
                && in_array($redirectUri, $client->getRedirectUri(), true) === false
279
            ) {
280 1
                $this->getEmitter()->emit(new RequestEvent(RequestEvent::CLIENT_AUTHENTICATION_FAILED, $request));
281 10
                throw OAuthServerException::invalidClient();
282
            }
283 2
        } elseif (is_array($client->getRedirectUri()) && count($client->getRedirectUri()) !== 1
284 2
            || empty($client->getRedirectUri())) {
285 1
            $this->getEmitter()->emit(new RequestEvent(RequestEvent::CLIENT_AUTHENTICATION_FAILED, $request));
286 1
            throw OAuthServerException::invalidClient();
287
        } else {
288 1
            $redirectUri = is_array($client->getRedirectUri())
289
                ? $client->getRedirectUri()[0]
290 1
                : $client->getRedirectUri();
291
        }
292
293 10
        $scopes = $this->validateScopes(
294 10
            $this->getQueryStringParameter('scope', $request, $this->defaultScope),
295 10
            $redirectUri
296
        );
297
298 10
        $stateParameter = $this->getQueryStringParameter('state', $request);
299
300 10
        $authorizationRequest = new AuthorizationRequest();
301 10
        $authorizationRequest->setGrantTypeId($this->getIdentifier());
302 10
        $authorizationRequest->setClient($client);
303 10
        $authorizationRequest->setRedirectUri($redirectUri);
304 10
        $authorizationRequest->setState($stateParameter);
305 10
        $authorizationRequest->setScopes($scopes);
306
307 10
        $codeChallenge = $this->getQueryStringParameter('code_challenge', $request);
308 10
        if ($this->shouldValidatePKCE($codeChallenge)) {
309 7
            if ($codeChallenge === null) {
310 1
                throw OAuthServerException::invalidRequest('code_challenge');
311
            }
312
313 6
            $codeChallengeMethod = $this->getQueryStringParameter('code_challenge_method', $request, 'plain');
314 6
            if (in_array($codeChallengeMethod, ['plain', 'S256'], true) === false) {
315 1
                throw OAuthServerException::invalidRequest(
316 1
                    'code_challenge_method',
317 1
                    'Code challenge method must be `plain` or `S256`'
318
                );
319
            }
320
321
            // Validate code_challenge according to RFC-7636
322
            // @see: https://tools.ietf.org/html/rfc7636#section-4.2
323 5
            if (preg_match('/^[A-Za-z0-9-._~]{43,128}$/', $codeChallenge) !== 1) {
324 4
                throw OAuthServerException::invalidRequest(
325 4
                    'code_challenged',
326 4
                    'Code challenge must follow the specifications of RFC-7636.'
327
                );
328
            }
329
330 1
            $authorizationRequest->setCodeChallenge($codeChallenge);
331 1
            $authorizationRequest->setCodeChallengeMethod($codeChallengeMethod);
332
        }
333
334 4
        return $authorizationRequest;
335
    }
336
337
    /**
338
     * {@inheritdoc}
339
     */
340 7
    public function completeAuthorizationRequest(AuthorizationRequest $authorizationRequest)
341
    {
342 7
        if ($authorizationRequest->getUser() instanceof UserEntityInterface === false) {
343 1
            throw new \LogicException('An instance of UserEntityInterface should be set on the AuthorizationRequest');
344
        }
345
346 6
        $finalRedirectUri = ($authorizationRequest->getRedirectUri() === null)
347 6
            ? is_array($authorizationRequest->getClient()->getRedirectUri())
348
                ? $authorizationRequest->getClient()->getRedirectUri()[0]
349 6
                : $authorizationRequest->getClient()->getRedirectUri()
350 6
            : $authorizationRequest->getRedirectUri();
351
352
        // The user approved the client, redirect them back with an auth code
353 6
        if ($authorizationRequest->isAuthorizationApproved() === true) {
354 5
            $authCode = $this->issueAuthCode(
355 5
                $this->authCodeTTL,
356 5
                $authorizationRequest->getClient(),
357 5
                $authorizationRequest->getUser()->getIdentifier(),
358 5
                $authorizationRequest->getRedirectUri(),
359 5
                $authorizationRequest->getScopes()
360
            );
361
362
            $payload = [
363 3
                'client_id'             => $authCode->getClient()->getIdentifier(),
364 3
                'redirect_uri'          => $authCode->getRedirectUri(),
365 3
                'auth_code_id'          => $authCode->getIdentifier(),
366 3
                'scopes'                => $authCode->getScopes(),
367 3
                'user_id'               => $authCode->getUserIdentifier(),
368 3
                'expire_time'           => (new \DateTime())->add($this->authCodeTTL)->format('U'),
369 3
                'code_challenge'        => $authorizationRequest->getCodeChallenge(),
370 3
                'code_challenge_method' => $authorizationRequest->getCodeChallengeMethod(),
371
            ];
372
373 3
            $response = new RedirectResponse();
374 3
            $response->setRedirectUri(
375 3
                $this->makeRedirectUri(
376 3
                    $finalRedirectUri,
377
                    [
378 3
                        'code'  => $this->encrypt(
379 3
                            json_encode(
380 3
                                $payload
381
                            )
382
                        ),
383 3
                        'state' => $authorizationRequest->getState(),
384
                    ]
385
                )
386
            );
387
388 3
            return $response;
389
        }
390
391
        // The user denied the client, redirect them back with an error
392 1
        throw OAuthServerException::accessDenied(
393 1
            'The user denied the request',
394 1
            $this->makeRedirectUri(
395 1
                $finalRedirectUri,
396
                [
397 1
                    'state' => $authorizationRequest->getState(),
398
                ]
399
            )
400
        );
401
    }
402
403
    /**
404
     * Check if PKCE should be validated, based on options.
405
     *
406
     * @param string|null $code
407
     *
408
     * @return bool
409
     */
410 22
    private function shouldValidatePKCE($code)
411
    {
412 22
        if ($this->enableCodeExchangeProof) {
413 13
            return true;
414
        }
415
416 9
        if ($code !== null && $this->enablePkceFallback) {
417 2
            return true;
418
        }
419
420 7
        return false;
421
    }
422
}
423