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

AuthCodeGrant::getClientRedirectUri()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 2.0625

Importance

Changes 0
Metric Value
dl 0
loc 6
ccs 3
cts 4
cp 0.75
rs 10
c 0
b 0
f 0
cc 2
nc 2
nop 1
crap 2.0625
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
     * @param AuthCodeRepositoryInterface     $authCodeRepository
38
     * @param RefreshTokenRepositoryInterface $refreshTokenRepository
39
     * @param \DateInterval                   $authCodeTTL
40
     *
41
     * @throws \Exception
42
     */
43 41
    public function __construct(
44
        AuthCodeRepositoryInterface $authCodeRepository,
45
        RefreshTokenRepositoryInterface $refreshTokenRepository,
46
        \DateInterval $authCodeTTL
47
    ) {
48 41
        $this->setAuthCodeRepository($authCodeRepository);
49 41
        $this->setRefreshTokenRepository($refreshTokenRepository);
50 41
        $this->authCodeTTL = $authCodeTTL;
51 41
        $this->refreshTokenTTL = new \DateInterval('P1M');
52 41
    }
53
54 13
    public function enableCodeExchangeProof()
55
    {
56 13
        $this->enableCodeExchangeProof = true;
57 13
    }
58
59
    /**
60
     * Respond to an access token request.
61
     *
62
     * @param ServerRequestInterface $request
63
     * @param ResponseTypeInterface  $responseType
64
     * @param \DateInterval          $accessTokenTTL
65
     *
66
     * @throws OAuthServerException
67
     *
68
     * @return ResponseTypeInterface
69
     */
70 18
    public function respondToAccessTokenRequest(
71
        ServerRequestInterface $request,
72
        ResponseTypeInterface $responseType,
73
        \DateInterval $accessTokenTTL
74
    ) {
75
        // Validate request
76 18
        $client = $this->validateClient($request);
77 18
        $encryptedAuthCode = $this->getRequestParameter('code', $request, null);
78
79 18
        if ($encryptedAuthCode === null) {
80 1
            throw OAuthServerException::invalidRequest('code');
81
        }
82
83
        try {
84 17
            $authCodePayload = json_decode($this->decrypt($encryptedAuthCode));
85
86 16
            $this->validateAuthorizationCode($authCodePayload, $client, $request);
87
88 11
            $scopes = $this->scopeRepository->finalizeScopes(
89 11
                $this->validateScopes($authCodePayload->scopes),
90 11
                $this->getIdentifier(),
91 11
                $client,
92 11
                $authCodePayload->user_id
93
            );
94 6
        } catch (\LogicException  $e) {
95 1
            throw OAuthServerException::invalidRequest('code', 'Cannot decrypt the authorization code');
96
        }
97
98
        // Validate code challenge
99 11
        if ($this->enableCodeExchangeProof === true) {
100 7
            $codeVerifier = $this->getRequestParameter('code_verifier', $request, null);
101
102 7
            if ($codeVerifier === null) {
103 1
                throw OAuthServerException::invalidRequest('code_verifier');
104
            }
105
106
            // Validate code_verifier according to RFC-7636
107
            // @see: https://tools.ietf.org/html/rfc7636#section-4.1
108 6
            if (preg_match('/^[A-Za-z0-9-._~]{43,128}$/', $codeVerifier) !== 1) {
109 3
                throw OAuthServerException::invalidRequest(
110 3
                    'code_verifier',
111 3
                    'Code Verifier must follow the specifications of RFC-7636.'
112
                );
113
            }
114
115 3
            switch ($authCodePayload->code_challenge_method) {
116 3
                case 'plain':
117 2
                    if (hash_equals($codeVerifier, $authCodePayload->code_challenge) === false) {
118 1
                        throw OAuthServerException::invalidGrant('Failed to verify `code_verifier`.');
119
                    }
120
121 1
                    break;
122 1
                case 'S256':
123
                    if (
124 1
                        hash_equals(
125 1
                            strtr(rtrim(base64_encode(hash('sha256', $codeVerifier, true)), '='), '+/', '-_'),
126 1
                            $authCodePayload->code_challenge
127 1
                        ) === false
128
                    ) {
129
                        throw OAuthServerException::invalidGrant('Failed to verify `code_verifier`.');
130
                    }
131
                    // @codeCoverageIgnoreStart
132
                    break;
133
                default:
134
                    throw OAuthServerException::serverError(
135
                        sprintf(
136
                            'Unsupported code challenge method `%s`',
137
                            $authCodePayload->code_challenge_method
138
                        )
139
                    );
140
                // @codeCoverageIgnoreEnd
141
            }
142
        }
143
144
        // Issue and persist access + refresh tokens
145 6
        $accessToken = $this->issueAccessToken($accessTokenTTL, $client, $authCodePayload->user_id, $scopes);
146 6
        $refreshToken = $this->issueRefreshToken($accessToken);
147
148
        // Send events to emitter
149 4
        $this->getEmitter()->emit(new RequestEvent(RequestEvent::ACCESS_TOKEN_ISSUED, $request));
150 4
        $this->getEmitter()->emit(new RequestEvent(RequestEvent::REFRESH_TOKEN_ISSUED, $request));
151
152
        // Inject tokens into response type
153 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 145 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...
154 4
        $responseType->setRefreshToken($refreshToken);
0 ignored issues
show
Bug introduced by
It seems like $refreshToken defined by $this->issueRefreshToken($accessToken) on line 146 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...
155
156
        // Revoke used auth code
157 4
        $this->authCodeRepository->revokeAuthCode($authCodePayload->auth_code_id);
158
159 4
        return $responseType;
160
    }
161
162
    /**
163
     * Validate the authorization code.
164
     *
165
     * @param \stdClass              $authCodePayload
166
     * @param ClientEntityInterface  $client
167
     * @param ServerRequestInterface $request
168
     */
169 16
    private function validateAuthorizationCode(
170
        $authCodePayload,
171
        ClientEntityInterface $client,
172
        ServerRequestInterface $request
173
    ) {
174 16
        if (time() > $authCodePayload->expire_time) {
175 1
            throw OAuthServerException::invalidRequest('code', 'Authorization code has expired');
176
        }
177
178 15
        if ($this->authCodeRepository->isAuthCodeRevoked($authCodePayload->auth_code_id) === true) {
179 1
            throw OAuthServerException::invalidRequest('code', 'Authorization code has been revoked');
180
        }
181
182 14
        if ($authCodePayload->client_id !== $client->getIdentifier()) {
183 1
            throw OAuthServerException::invalidRequest('code', 'Authorization code was not issued to this client');
184
        }
185
186
        // The redirect URI is required in this request
187 13
        $redirectUri = $this->getRequestParameter('redirect_uri', $request, null);
188 13
        if (empty($authCodePayload->redirect_uri) === false && $redirectUri === null) {
189 1
            throw OAuthServerException::invalidRequest('redirect_uri');
190
        }
191
192 12
        if ($authCodePayload->redirect_uri !== $redirectUri) {
193 1
            throw OAuthServerException::invalidRequest('redirect_uri', 'Invalid redirect URI');
194
        }
195 11
    }
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 ($clientId === null) {
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 (empty($client->getRedirectUri()) ||
251 2
            (\is_array($client->getRedirectUri()) && \count($client->getRedirectUri()) !== 1)) {
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()
319 6
                          ?? $this->getClientRedirectUri($authorizationRequest);
320
321
        // The user approved the client, redirect them back with an auth code
322 6
        if ($authorizationRequest->isAuthorizationApproved() === true) {
323 5
            $authCode = $this->issueAuthCode(
324 5
                $this->authCodeTTL,
325 5
                $authorizationRequest->getClient(),
326 5
                $authorizationRequest->getUser()->getIdentifier(),
327 5
                $authorizationRequest->getRedirectUri(),
328 5
                $authorizationRequest->getScopes()
329
            );
330
331
            $payload = [
332 3
                'client_id'             => $authCode->getClient()->getIdentifier(),
333 3
                'redirect_uri'          => $authCode->getRedirectUri(),
334 3
                'auth_code_id'          => $authCode->getIdentifier(),
335 3
                'scopes'                => $authCode->getScopes(),
336 3
                'user_id'               => $authCode->getUserIdentifier(),
337 3
                'expire_time'           => (new \DateTime())->add($this->authCodeTTL)->format('U'),
338 3
                'code_challenge'        => $authorizationRequest->getCodeChallenge(),
339 3
                'code_challenge_method' => $authorizationRequest->getCodeChallengeMethod(),
340
            ];
341
342 3
            $response = new RedirectResponse();
343 3
            $response->setRedirectUri(
344 3
                $this->makeRedirectUri(
345 3
                    $finalRedirectUri,
346
                    [
347 3
                        'code'  => $this->encrypt(
348 3
                            json_encode(
349 3
                                $payload
350
                            )
351
                        ),
352 3
                        'state' => $authorizationRequest->getState(),
353
                    ]
354
                )
355
            );
356
357 3
            return $response;
358
        }
359
360
        // The user denied the client, redirect them back with an error
361 1
        throw OAuthServerException::accessDenied(
362 1
            'The user denied the request',
363 1
            $this->makeRedirectUri(
364 1
                $finalRedirectUri,
365
                [
366 1
                    'state' => $authorizationRequest->getState(),
367
                ]
368
            )
369
        );
370
    }
371
372
    /**
373
     * Get the client redirect URI if not set in the request.
374
     *
375
     * @param AuthorizationRequest $authorizationRequest
376
     *
377
     * @return string
378
     */
379 6
    private function getClientRedirectUri(AuthorizationRequest $authorizationRequest)
380
    {
381 6
        return \is_array($authorizationRequest->getClient()->getRedirectUri())
382
                ? $authorizationRequest->getClient()->getRedirectUri()[0]
383 6
                : $authorizationRequest->getClient()->getRedirectUri();
384
    }
385
}
386