Passed
Pull Request — master (#1122)
by Sebastian
02:07
created

AuthCodeGrant::validateAuthorizationCode()   B

Complexity

Conditions 8
Paths 7

Size

Total Lines 29
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 14
CRAP Score 8

Importance

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