Completed
Pull Request — master (#1000)
by Andrew
02:29
created

AuthCodeGrant::getIdentifier()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

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