Completed
Pull Request — master (#924)
by
unknown
02:27
created

AuthCodeGrant::getExtraAuthCodeParams()   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
dl 0
loc 4
ccs 2
cts 2
cp 1
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 1
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 League\OAuth2\Server\Entities\ClientEntityInterface;
13
use League\OAuth2\Server\Entities\ScopeEntityInterface;
14
use League\OAuth2\Server\Entities\UserEntityInterface;
15
use League\OAuth2\Server\Exception\OAuthServerException;
16
use League\OAuth2\Server\Repositories\AuthCodeRepositoryInterface;
17
use League\OAuth2\Server\Repositories\RefreshTokenRepositoryInterface;
18
use League\OAuth2\Server\RequestEvent;
19
use League\OAuth2\Server\RequestTypes\AuthorizationRequest;
20
use League\OAuth2\Server\ResponseTypes\RedirectResponse;
21
use League\OAuth2\Server\ResponseTypes\ResponseTypeInterface;
22
use Psr\Http\Message\ServerRequestInterface;
23
24
class AuthCodeGrant extends AbstractAuthorizeGrant
25
{
26
    /**
27
     * @var \DateInterval
28
     */
29
    private $authCodeTTL;
30
31
    /**
32
     * @var bool
33
     */
34
    private $enableCodeExchangeProof = false;
35
36
    /**
37
     * @param AuthCodeRepositoryInterface     $authCodeRepository
38
     * @param RefreshTokenRepositoryInterface $refreshTokenRepository
39
     * @param \DateInterval                   $authCodeTTL
40
     */
41 19
    public function __construct(
42
        AuthCodeRepositoryInterface $authCodeRepository,
43
        RefreshTokenRepositoryInterface $refreshTokenRepository,
44
        \DateInterval $authCodeTTL
45
    ) {
46 19
        $this->setAuthCodeRepository($authCodeRepository);
47 19
        $this->setRefreshTokenRepository($refreshTokenRepository);
48 19
        $this->authCodeTTL = $authCodeTTL;
49 19
        $this->refreshTokenTTL = new \DateInterval('P1M');
50 19
    }
51
52 6
    public function enableCodeExchangeProof()
53
    {
54 6
        $this->enableCodeExchangeProof = true;
55 6
    }
56
57
    /**
58
     * Respond to an access token request.
59
     *
60
     * @param ServerRequestInterface $request
61
     * @param ResponseTypeInterface  $responseType
62
     * @param \DateInterval          $accessTokenTTL
63
     *
64
     * @throws OAuthServerException
65
     *
66
     * @return ResponseTypeInterface
67
     */
68
    public function respondToAccessTokenRequest(
69
        ServerRequestInterface $request,
70
        ResponseTypeInterface $responseType,
71
        \DateInterval $accessTokenTTL
72
    ) {
73
        // Validate request
74
        $client = $this->validateClient($request);
75
        $encryptedAuthCode = $this->getRequestParameter('code', $request, null);
76
77
        if ($encryptedAuthCode === null) {
78
            throw OAuthServerException::invalidRequest('code');
79
        }
80
81
        // Validate the authorization code
82
        try {
83
            $authCodePayload = json_decode($this->decrypt($encryptedAuthCode));
84
            if (time() > $authCodePayload->expire_time) {
85
                throw OAuthServerException::invalidRequest('code', 'Authorization code has expired');
86
            }
87
88
            if ($this->authCodeRepository->isAuthCodeRevoked($authCodePayload->auth_code_id) === true) {
89
                throw OAuthServerException::invalidRequest('code', 'Authorization code has been revoked');
90
            }
91
92
            if ($authCodePayload->client_id !== $client->getIdentifier()) {
93
                throw OAuthServerException::invalidRequest('code', 'Authorization code was not issued to this client');
94
            }
95
96
            // The redirect URI is required in this request
97
            $redirectUri = $this->getRequestParameter('redirect_uri', $request, null);
98
            if (empty($authCodePayload->redirect_uri) === false && $redirectUri === null) {
99
                throw OAuthServerException::invalidRequest('redirect_uri');
100
            }
101
102
            if ($authCodePayload->redirect_uri !== $redirectUri) {
103
                throw OAuthServerException::invalidRequest('redirect_uri', 'Invalid redirect URI');
104
            }
105
106
            $scopes = [];
107
            foreach ($authCodePayload->scopes as $scopeId) {
108
                $scope = $this->scopeRepository->getScopeEntityByIdentifier($scopeId);
109
110
                if ($scope instanceof ScopeEntityInterface === false) {
111
                    // @codeCoverageIgnoreStart
112
                    throw OAuthServerException::invalidScope($scopeId);
113
                    // @codeCoverageIgnoreEnd
114
                }
115
116
                $scopes[] = $scope;
117
            }
118
119
            // Finalize the requested scopes
120
            $scopes = $this->scopeRepository->finalizeScopes(
121
                $scopes,
122
                $this->getIdentifier(),
123
                $client,
124
                $authCodePayload->user_id
125
            );
126
        } catch (\LogicException  $e) {
127
            throw OAuthServerException::invalidRequest('code', 'Cannot decrypt the authorization code');
128
        }
129
130
        // Validate code challenge
131
        if ($this->enableCodeExchangeProof === true) {
132
            $codeVerifier = $this->getRequestParameter('code_verifier', $request, null);
133
            if ($codeVerifier === null) {
134
                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
            if (preg_match('/^[A-Za-z0-9-._~]{43,128}$/', $codeVerifier) !== 1) {
140
                throw OAuthServerException::invalidRequest(
141
                    'code_verifier',
142
                    'Code Verifier must follow the specifications of RFC-7636.'
143
                );
144
            }
145
146
            switch ($authCodePayload->code_challenge_method) {
147
                case 'plain':
148
                    if (hash_equals($codeVerifier, $authCodePayload->code_challenge) === false) {
149
                        throw OAuthServerException::invalidGrant('Failed to verify `code_verifier`.');
150
                    }
151
152
                    break;
153
                case 'S256':
154
                    if (
155
                        hash_equals(
156
                            strtr(rtrim(base64_encode(hash('sha256', $codeVerifier, true)), '='), '+/', '-_'),
157
                            $authCodePayload->code_challenge
158
                        ) === false
159
                    ) {
160
                        throw OAuthServerException::invalidGrant('Failed to verify `code_verifier`.');
161
                    }
162
                    // @codeCoverageIgnoreStart
163
                    break;
164
                default:
165
                    throw OAuthServerException::serverError(
166
                        sprintf(
167
                            'Unsupported code challenge method `%s`',
168
                            $authCodePayload->code_challenge_method
169
                        )
170
                    );
171
                // @codeCoverageIgnoreEnd
172
            }
173
        }
174
175
        // Handle extra authorization code parameters
176
        $this->handleExtraAuthCodeParams($authCodePayload);
177
178
        // Issue and persist access + refresh tokens
179
        $accessToken = $this->issueAccessToken($accessTokenTTL, $client, $authCodePayload->user_id, $scopes);
180
        $refreshToken = $this->issueRefreshToken($accessToken);
181
182
        // Send events to emitter
183
        $this->getEmitter()->emit(new RequestEvent(RequestEvent::ACCESS_TOKEN_ISSUED, $request));
184
        $this->getEmitter()->emit(new RequestEvent(RequestEvent::REFRESH_TOKEN_ISSUED, $request));
185
186
        // Inject tokens into response type
187
        $responseType->setAccessToken($accessToken);
0 ignored issues
show
Bug introduced by
It seems like $accessToken defined by $this->issueAccessToken(...load->user_id, $scopes) on line 179 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...
188
        $responseType->setRefreshToken($refreshToken);
0 ignored issues
show
Bug introduced by
It seems like $refreshToken defined by $this->issueRefreshToken($accessToken) on line 180 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...
189
190
        // Revoke used auth code
191
        $this->authCodeRepository->revokeAuthCode($authCodePayload->auth_code_id);
192
193
        return $responseType;
194
    }
195
196
    /**
197
     * Return the grant identifier that can be used in matching up requests.
198
     *
199
     * @return string
200
     */
201 15
    public function getIdentifier()
202
    {
203 15
        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 (is_null($clientId)) {
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 (is_array($client->getRedirectUri()) && count($client->getRedirectUri()) !== 1
250 2
            || empty($client->getRedirectUri())) {
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 3
    public function completeAuthorizationRequest(AuthorizationRequest $authorizationRequest)
312
    {
313 3
        if ($authorizationRequest->getUser() instanceof UserEntityInterface === false) {
314
            throw new \LogicException('An instance of UserEntityInterface should be set on the AuthorizationRequest');
315
        }
316
317 3
        $finalRedirectUri = ($authorizationRequest->getRedirectUri() === null)
318 3
            ? is_array($authorizationRequest->getClient()->getRedirectUri())
319
                ? $authorizationRequest->getClient()->getRedirectUri()[0]
320 3
                : $authorizationRequest->getClient()->getRedirectUri()
321 3
            : $authorizationRequest->getRedirectUri();
322
323
        // The user approved the client, redirect them back with an auth code
324 3
        if ($authorizationRequest->isAuthorizationApproved() === true) {
325 2
            $authCode = $this->issueAuthCode(
326 2
                $this->authCodeTTL,
327 2
                $authorizationRequest->getClient(),
328 2
                $authorizationRequest->getUser()->getIdentifier(),
329 2
                $authorizationRequest->getRedirectUri(),
330 2
                $authorizationRequest->getScopes()
331
            );
332
333
            $payload = [
334 2
                'client_id'             => $authCode->getClient()->getIdentifier(),
335 2
                'redirect_uri'          => $authCode->getRedirectUri(),
336 2
                'auth_code_id'          => $authCode->getIdentifier(),
337 2
                'scopes'                => $authCode->getScopes(),
338 2
                'user_id'               => $authCode->getUserIdentifier(),
339 2
                'expire_time'           => (new \DateTime())->add($this->authCodeTTL)->format('U'),
340 2
                'code_challenge'        => $authorizationRequest->getCodeChallenge(),
341 2
                'code_challenge_method' => $authorizationRequest->getCodeChallengeMethod(),
342
            ];
343
344 2
            $payload = array_merge($this->getExtraAuthCodeParams($authorizationRequest), $payload);
345
346 2
            $response = new RedirectResponse();
347 2
            $response->setRedirectUri(
348 2
                $this->makeRedirectUri(
349 2
                    $finalRedirectUri,
350
                    [
351 2
                        'code'  => $this->encrypt(
352 2
                            json_encode(
353 2
                                $payload
354
                            )
355
                        ),
356 2
                        'state' => $authorizationRequest->getState(),
357
                    ]
358
                )
359
            );
360
361 2
            return $response;
362
        }
363
364
        // The user denied the client, redirect them back with an error
365 1
        throw OAuthServerException::accessDenied(
366 1
            'The user denied the request',
367 1
            $this->makeRedirectUri(
368 1
                $finalRedirectUri,
369
                [
370 1
                    'state' => $authorizationRequest->getState(),
371
                ]
372
            )
373
        );
374
    }
375
376
    /**
377
     * Add custom fields to your authorization code to save some data from the previous (authorize) state
378
     * for when you are issuing the token at the token endpoint
379
     *
380
     * @param AuthorizationRequest $authorizationRequest
381
     *
382
     * @return array
383
     */
384 2
    protected function getExtraAuthCodeParams(AuthorizationRequest $authorizationRequest)
385
    {
386 2
        return [];
387
    }
388
389
    /**
390
     * Handle the extra params specified in getExtraAuthCodeParams
391
     *
392
     * @param object $authCodePayload
393
     */
394
    protected function handleExtraAuthCodeParams(object $authCodePayload)
395
    {
396
    }
397
}
398