Completed
Pull Request — master (#924)
by
unknown
02:04
created

AuthCodeGrant::getExtraAuthCodeParams()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

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