Completed
Push — master ( 0bdd02...a34f5d )
by Andrew
24s
created

AuthCodeGrant::validateAuthorizationCode()   B

Complexity

Conditions 7
Paths 6

Size

Total Lines 27

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 13
CRAP Score 7

Importance

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