Completed
Pull Request — master (#953)
by Andrew
01:58
created

AuthCodeGrant::getScopes()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 18

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 3

Importance

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