Passed
Pull Request — master (#1329)
by Andrew
49:52 queued 14:52
created

AuthCodeGrant::getClientRedirectUri()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

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