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

AuthCodeGrant::getIdentifier()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

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