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

AuthCodeGrant::completeAuthorizationRequest()   B

Complexity

Conditions 5
Paths 9

Size

Total Lines 62
Code Lines 38

Duplication

Lines 0
Ratio 0 %

Importance

Changes 5
Bugs 0 Features 0
Metric Value
c 5
b 0
f 0
dl 0
loc 62
rs 8.6652
cc 5
eloc 38
nc 9
nop 1

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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 $forceEnabledCodeExchangeProof = true;
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 disableForceEnabledCodeExchangeProof()
63
    {
64
        $this->forceEnabledCodeExchangeProof = false;
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 (
142
            $this->enableCodeExchangeProof === true
143
            && (($codeVerifier = $this->getRequestParameter('code_verifier', $request, null)) !== null || $this->forceEnabledCodeExchangeProof === true)
144
        ) {
145
            if ($codeVerifier === null) {
146
                throw OAuthServerException::invalidRequest('code_verifier');
147
            }
148
149
            switch ($authCodePayload->code_challenge_method) {
150
                case 'plain':
151
                    if (hash_equals($codeVerifier, $authCodePayload->code_challenge) === false) {
152
                        throw OAuthServerException::invalidGrant('Failed to verify `code_verifier`.');
153
                    }
154
155
                    break;
156
                case 'S256':
157
                    if (
158
                        hash_equals(
159
                            urlencode(base64_encode(hash('sha256', $codeVerifier))),
160
                            $authCodePayload->code_challenge
161
                        ) === false
162
                    ) {
163
                        throw OAuthServerException::invalidGrant('Failed to verify `code_verifier`.');
164
                    }
165
                    // @codeCoverageIgnoreStart
166
                    break;
167
                default:
168
                    throw OAuthServerException::serverError(
169
                        sprintf(
170
                            'Unsupported code challenge method `%s`',
171
                            $authCodePayload->code_challenge_method
172
                        )
173
                    );
174
                // @codeCoverageIgnoreEnd
175
            }
176
        }
177
178
        // Issue and persist access + refresh tokens
179
        $accessToken = $this->issueAccessToken($accessTokenTTL, $client, $authCodePayload->user_id, $scopes);
180
        $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 179 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...
181
182
        // Inject tokens into response type
183
        $responseType->setAccessToken($accessToken);
0 ignored issues
show
Bug introduced by
It seems like $accessToken defined by $this->issueAccessToken(...load->user_id, $scopes) on line 179 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...
184
        $responseType->setRefreshToken($refreshToken);
0 ignored issues
show
Bug introduced by
It seems like $refreshToken defined by $this->issueRefreshToken($accessToken) on line 180 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...
185
186
        // Revoke used auth code
187
        $this->authCodeRepository->revokeAuthCode($authCodePayload->auth_code_id);
188
189
        return $responseType;
190
    }
191
192
    /**
193
     * Return the grant identifier that can be used in matching up requests.
194
     *
195
     * @return string
196
     */
197
    public function getIdentifier()
198
    {
199
        return 'authorization_code';
200
    }
201
202
    /**
203
     * {@inheritdoc}
204
     */
205
    public function canRespondToAuthorizationRequest(ServerRequestInterface $request)
206
    {
207
        return (
208
            array_key_exists('response_type', $request->getQueryParams())
209
            && $request->getQueryParams()['response_type'] === 'code'
210
            && isset($request->getQueryParams()['client_id'])
211
        );
212
    }
213
214
    /**
215
     * {@inheritdoc}
216
     */
217
    public function validateAuthorizationRequest(ServerRequestInterface $request)
218
    {
219
        $clientId = $this->getQueryStringParameter(
220
            'client_id',
221
            $request,
222
            $this->getServerParameter('PHP_AUTH_USER', $request)
223
        );
224
        if (is_null($clientId)) {
225
            throw OAuthServerException::invalidRequest('client_id');
226
        }
227
228
        $client = $this->clientRepository->getClientEntity(
229
            $clientId,
230
            $this->getIdentifier(),
231
            null,
232
            false
233
        );
234
235
        if ($client instanceof ClientEntityInterface === false) {
236
            $this->getEmitter()->emit(new RequestEvent(RequestEvent::CLIENT_AUTHENTICATION_FAILED, $request));
237
            throw OAuthServerException::invalidClient();
238
        }
239
240
        $redirectUri = $this->getQueryStringParameter('redirect_uri', $request);
241
        if ($redirectUri !== null) {
242
            if (
243
                is_string($client->getRedirectUri())
244
                && (strcmp($client->getRedirectUri(), $redirectUri) !== 0)
245
            ) {
246
                $this->getEmitter()->emit(new RequestEvent(RequestEvent::CLIENT_AUTHENTICATION_FAILED, $request));
247
                throw OAuthServerException::invalidClient();
248
            } elseif (
249
                is_array($client->getRedirectUri())
250
                && in_array($redirectUri, $client->getRedirectUri()) === false
251
            ) {
252
                $this->getEmitter()->emit(new RequestEvent(RequestEvent::CLIENT_AUTHENTICATION_FAILED, $request));
253
                throw OAuthServerException::invalidClient();
254
            }
255
        } elseif (is_array($client->getRedirectUri()) && count($client->getRedirectUri()) !== 1
256
            || empty($client->getRedirectUri())
257
        ) {
258
            $this->getEmitter()->emit(new RequestEvent(RequestEvent::CLIENT_AUTHENTICATION_FAILED, $request));
259
            throw OAuthServerException::invalidClient();
260
        }
261
262
        $scopes = $this->validateScopes(
263
            $this->getQueryStringParameter('scope', $request, $this->defaultScope),
264
            is_array($client->getRedirectUri())
265
                ? $client->getRedirectUri()[0]
266
                : $client->getRedirectUri()
267
        );
268
269
        $stateParameter = $this->getQueryStringParameter('state', $request);
270
271
        $authorizationRequest = new AuthorizationRequest();
272
        $authorizationRequest->setGrantTypeId($this->getIdentifier());
273
        $authorizationRequest->setClient($client);
274
        $authorizationRequest->setRedirectUri($redirectUri);
275
        $authorizationRequest->setState($stateParameter);
276
        $authorizationRequest->setScopes($scopes);
277
278
        if (
279
            $this->enableCodeExchangeProof === true
280
            && (($codeChallenge = $this->getQueryStringParameter('code_challenge', $request)) !== null || $this->forceEnabledCodeExchangeProof === true)
281
        ) {
282
            if ($codeChallenge === null) {
283
                throw OAuthServerException::invalidRequest('code_challenge');
284
            }
285
286
            if (preg_match('/^[A-Za-z0-9-._~]{43,128}$/', $codeChallenge) !== 1) {
287
                throw OAuthServerException::invalidRequest(
288
                    'code_challenge',
289
                    'The code_challenge must be between 43 and 128 characters'
290
                );
291
            }
292
293
            $codeChallengeMethod = $this->getQueryStringParameter('code_challenge_method', $request, 'plain');
294
            if (in_array($codeChallengeMethod, ['plain', 'S256']) === false) {
295
                throw OAuthServerException::invalidRequest(
296
                    'code_challenge_method',
297
                    'Code challenge method must be `plain` or `S256`'
298
                );
299
            }
300
301
            $authorizationRequest->setCodeChallenge($codeChallenge);
302
            $authorizationRequest->setCodeChallengeMethod($codeChallengeMethod);
303
        }
304
305
        return $authorizationRequest;
306
    }
307
308
    /**
309
     * {@inheritdoc}
310
     */
311
    public function completeAuthorizationRequest(AuthorizationRequest $authorizationRequest)
312
    {
313
        if ($authorizationRequest->getUser() instanceof UserEntityInterface === false) {
314
            throw new \LogicException('An instance of UserEntityInterface should be set on the AuthorizationRequest');
315
        }
316
317
        $finalRedirectUri = ($authorizationRequest->getRedirectUri() === null)
318
            ? is_array($authorizationRequest->getClient()->getRedirectUri())
319
                ? $authorizationRequest->getClient()->getRedirectUri()[0]
320
                : $authorizationRequest->getClient()->getRedirectUri()
321
            : $authorizationRequest->getRedirectUri();
322
323
        // The user approved the client, redirect them back with an auth code
324
        if ($authorizationRequest->isAuthorizationApproved() === true) {
325
            $authCode = $this->issueAuthCode(
326
                $this->authCodeTTL,
327
                $authorizationRequest->getClient(),
328
                $authorizationRequest->getUser()->getIdentifier(),
329
                $authorizationRequest->getRedirectUri(),
330
                $authorizationRequest->getScopes()
331
            );
332
333
            $payload = [
334
                'client_id'             => $authCode->getClient()->getIdentifier(),
335
                'redirect_uri'          => $authCode->getRedirectUri(),
336
                'auth_code_id'          => $authCode->getIdentifier(),
337
                'scopes'                => $authCode->getScopes(),
338
                'user_id'               => $authCode->getUserIdentifier(),
339
                'expire_time'           => (new \DateTime())->add($this->authCodeTTL)->format('U'),
340
                'code_challenge'        => $authorizationRequest->getCodeChallenge(),
341
                'code_challenge_method' => $authorizationRequest->getCodeChallengeMethod(),
342
            ];
343
344
            $response = new RedirectResponse();
345
            $response->setRedirectUri(
346
                $this->makeRedirectUri(
347
                    $finalRedirectUri,
348
                    [
349
                        'code'  => $this->encrypt(
350
                            json_encode(
351
                                $payload
352
                            )
353
                        ),
354
                        'state' => $authorizationRequest->getState(),
355
                    ]
356
                )
357
            );
358
359
            return $response;
360
        }
361
362
        // The user denied the client, redirect them back with an error
363
        throw OAuthServerException::accessDenied(
364
            'The user denied the request',
365
            $this->makeRedirectUri(
366
                $finalRedirectUri,
367
                [
368
                    'state' => $authorizationRequest->getState(),
369
                ]
370
            )
371
        );
372
    }
373
}
374