Passed
Pull Request — master (#1110)
by Patrick
02:06
created

AuthCodeGrant::__construct()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 17
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 2

Importance

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