Completed
Pull Request — master (#817)
by
unknown
33:46
created

AuthCodeGrant::canRespondToAuthorizationRequest()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 8
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

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