Completed
Pull Request — master (#795)
by
unknown
34:50
created

AuthCodeGrant::enableCodeExchangeProof()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 4
c 0
b 0
f 0
rs 10
cc 1
eloc 2
nc 1
nop 0
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
    public function __construct(
42
        AuthCodeRepositoryInterface $authCodeRepository,
43
        RefreshTokenRepositoryInterface $refreshTokenRepository,
44
        \DateInterval $authCodeTTL
45
    ) {
46
        $this->setAuthCodeRepository($authCodeRepository);
47
        $this->setRefreshTokenRepository($refreshTokenRepository);
48
        $this->authCodeTTL = $authCodeTTL;
49
        $this->refreshTokenTTL = new \DateInterval('P1M');
50
    }
51
52
    public function enableCodeExchangeProof()
53
    {
54
        $this->enableCodeExchangeProof = true;
55
    }
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
85
            $this->validateAuthCodePayload($authCodePayload);
86
87
            if (time() > $authCodePayload->expire_time) {
88
                throw OAuthServerException::invalidRequest('code', 'Authorization code has expired');
89
            }
90
91
            if ($this->authCodeRepository->isAuthCodeRevoked($authCodePayload->auth_code_id) === true) {
92
                throw OAuthServerException::invalidRequest('code', 'Authorization code has been revoked');
93
            }
94
95
            if ($authCodePayload->client_id !== $client->getIdentifier()) {
96
                throw OAuthServerException::invalidRequest('code', 'Authorization code was not issued to this client');
97
            }
98
99
            // The redirect URI is required in this request
100
            $redirectUri = $this->getRequestParameter('redirect_uri', $request, null);
101
            if (empty($authCodePayload->redirect_uri) === false && $redirectUri === null) {
102
                throw OAuthServerException::invalidRequest('redirect_uri');
103
            }
104
105
            if ($authCodePayload->redirect_uri !== $redirectUri) {
106
                throw OAuthServerException::invalidRequest('redirect_uri', 'Invalid redirect URI');
107
            }
108
109
            $scopes = [];
110
            foreach ($authCodePayload->scopes as $scopeId) {
111
                $scope = $this->scopeRepository->getScopeEntityByIdentifier($scopeId);
112
113
                if ($scope instanceof ScopeEntityInterface === false) {
114
                    // @codeCoverageIgnoreStart
115
                    throw OAuthServerException::invalidScope($scopeId);
116
                    // @codeCoverageIgnoreEnd
117
                }
118
119
                $scopes[] = $scope;
120
            }
121
122
            // Finalize the requested scopes
123
            $scopes = $this->scopeRepository->finalizeScopes(
124
                $scopes,
125
                $this->getIdentifier(),
126
                $client,
127
                $authCodePayload->user_id
128
            );
129
        } catch (\LogicException  $e) {
130
            throw OAuthServerException::invalidRequest('code', 'Cannot decrypt the authorization code');
131
        }
132
133
        // Validate code challenge
134
        if ($this->enableCodeExchangeProof === true) {
135
            $codeVerifier = $this->getRequestParameter('code_verifier', $request, null);
136
            if ($codeVerifier === null) {
137
                throw OAuthServerException::invalidRequest('code_verifier');
138
            }
139
140
            switch ($authCodePayload->code_challenge_method) {
141
                case 'plain':
142
                    if (hash_equals($codeVerifier, $authCodePayload->code_challenge) === false) {
143
                        throw OAuthServerException::invalidGrant('Failed to verify `code_verifier`.');
144
                    }
145
146
                    break;
147
                case 'S256':
148
                    if (
149
                        hash_equals(
150
                            urlencode(base64_encode(hash('sha256', $codeVerifier))),
151
                            $authCodePayload->code_challenge
152
                        ) === false
153
                    ) {
154
                        throw OAuthServerException::invalidGrant('Failed to verify `code_verifier`.');
155
                    }
156
                    // @codeCoverageIgnoreStart
157
                    break;
158
                default:
159
                    throw OAuthServerException::serverError(
160
                        sprintf(
161
                            'Unsupported code challenge method `%s`',
162
                            $authCodePayload->code_challenge_method
163
                        )
164
                    );
165
                // @codeCoverageIgnoreEnd
166
            }
167
        }
168
169
        // Issue and persist access + refresh tokens
170
        $accessToken = $this->issueAccessToken($accessTokenTTL, $client, $authCodePayload->user_id, $scopes);
171
        $refreshToken = $this->issueRefreshToken($accessToken);
0 ignored issues
show
Bug introduced by
It seems like $accessToken defined by $this->issueAccessToken(...load->user_id, $scopes) on line 170 can be null; however, League\OAuth2\Server\Gra...nt::issueRefreshToken() 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...
172
173
        // Inject tokens into response type
174
        $responseType->setAccessToken($accessToken);
0 ignored issues
show
Bug introduced by
It seems like $accessToken defined by $this->issueAccessToken(...load->user_id, $scopes) on line 170 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...
175
        $responseType->setRefreshToken($refreshToken);
0 ignored issues
show
Bug introduced by
It seems like $refreshToken defined by $this->issueRefreshToken($accessToken) on line 171 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...
176
177
        // Revoke used auth code
178
        $this->authCodeRepository->revokeAuthCode($authCodePayload->auth_code_id);
179
180
        return $responseType;
181
    }
182
183
    /**
184
     * Validate auth code payload to avoid errors like 'Undefined property: stdClass::$auth_code_id'.
185
     *
186
     * @param \stdClass $authCodePayload
187
     *
188
     * @throws OAuthServerException
189
     */
190
    private function validateAuthCodePayload(\stdClass $authCodePayload)
191
    {
192
        $errors = [
193
            isset($authCodePayload->expire_time),
194
            isset($authCodePayload->auth_code_id),
195
            isset($authCodePayload->client_id),
196
            isset($authCodePayload->redirect_uri),
197
            isset($authCodePayload->scopes) && is_array($authCodePayload->scopes),
198
            isset($authCodePayload->user_id)
199
        ];
200
201
        if (in_array(false, $errors, true)) {
202
            throw OAuthServerException::invalidRequest('code');
203
        }
204
    }
205
206
    /**
207
     * Return the grant identifier that can be used in matching up requests.
208
     *
209
     * @return string
210
     */
211
    public function getIdentifier()
212
    {
213
        return 'authorization_code';
214
    }
215
216
    /**
217
     * {@inheritdoc}
218
     */
219
    public function canRespondToAuthorizationRequest(ServerRequestInterface $request)
220
    {
221
        return (
222
            array_key_exists('response_type', $request->getQueryParams())
223
            && $request->getQueryParams()['response_type'] === 'code'
224
            && isset($request->getQueryParams()['client_id'])
225
        );
226
    }
227
228
    /**
229
     * {@inheritdoc}
230
     */
231
    public function validateAuthorizationRequest(ServerRequestInterface $request)
232
    {
233
        $clientId = $this->getQueryStringParameter(
234
            'client_id',
235
            $request,
236
            $this->getServerParameter('PHP_AUTH_USER', $request)
237
        );
238
        if (is_null($clientId)) {
239
            throw OAuthServerException::invalidRequest('client_id');
240
        }
241
242
        $client = $this->clientRepository->getClientEntity(
243
            $clientId,
244
            $this->getIdentifier(),
245
            null,
246
            false
247
        );
248
249
        if ($client instanceof ClientEntityInterface === false) {
250
            $this->getEmitter()->emit(new RequestEvent(RequestEvent::CLIENT_AUTHENTICATION_FAILED, $request));
251
            throw OAuthServerException::invalidClient();
252
        }
253
254
        $redirectUri = $this->getQueryStringParameter('redirect_uri', $request);
255
        if ($redirectUri !== null) {
256
            if (
257
                is_string($client->getRedirectUri())
258
                && (strcmp($client->getRedirectUri(), $redirectUri) !== 0)
259
            ) {
260
                $this->getEmitter()->emit(new RequestEvent(RequestEvent::CLIENT_AUTHENTICATION_FAILED, $request));
261
                throw OAuthServerException::invalidClient();
262
            } elseif (
263
                is_array($client->getRedirectUri())
264
                && in_array($redirectUri, $client->getRedirectUri()) === false
265
            ) {
266
                $this->getEmitter()->emit(new RequestEvent(RequestEvent::CLIENT_AUTHENTICATION_FAILED, $request));
267
                throw OAuthServerException::invalidClient();
268
            }
269
        }
270
271
        $scopes = $this->validateScopes(
272
            $this->getQueryStringParameter('scope', $request),
273
            is_array($client->getRedirectUri())
274
                ? $client->getRedirectUri()[0]
275
                : $client->getRedirectUri()
276
        );
277
278
        $stateParameter = $this->getQueryStringParameter('state', $request);
279
280
        $authorizationRequest = new AuthorizationRequest();
281
        $authorizationRequest->setGrantTypeId($this->getIdentifier());
282
        $authorizationRequest->setClient($client);
283
        $authorizationRequest->setRedirectUri($redirectUri);
284
        $authorizationRequest->setState($stateParameter);
285
        $authorizationRequest->setScopes($scopes);
286
287
        if ($this->enableCodeExchangeProof === true) {
288
            $codeChallenge = $this->getQueryStringParameter('code_challenge', $request);
289
            if ($codeChallenge === null) {
290
                throw OAuthServerException::invalidRequest('code_challenge');
291
            }
292
293
            if (preg_match('/^[A-Za-z0-9-._~]{43,128}$/', $codeChallenge) !== 1) {
294
                throw OAuthServerException::invalidRequest(
295
                    'code_challenge',
296
                    'The code_challenge must be between 43 and 128 characters'
297
                );
298
            }
299
300
            $codeChallengeMethod = $this->getQueryStringParameter('code_challenge_method', $request, 'plain');
301
            if (in_array($codeChallengeMethod, ['plain', 'S256']) === false) {
302
                throw OAuthServerException::invalidRequest(
303
                    'code_challenge_method',
304
                    'Code challenge method must be `plain` or `S256`'
305
                );
306
            }
307
308
            $authorizationRequest->setCodeChallenge($codeChallenge);
309
            $authorizationRequest->setCodeChallengeMethod($codeChallengeMethod);
310
        }
311
312
        return $authorizationRequest;
313
    }
314
315
    /**
316
     * {@inheritdoc}
317
     */
318
    public function completeAuthorizationRequest(AuthorizationRequest $authorizationRequest)
319
    {
320
        if ($authorizationRequest->getUser() instanceof UserEntityInterface === false) {
321
            throw new \LogicException('An instance of UserEntityInterface should be set on the AuthorizationRequest');
322
        }
323
324
        $finalRedirectUri = ($authorizationRequest->getRedirectUri() === null)
325
            ? is_array($authorizationRequest->getClient()->getRedirectUri())
326
                ? $authorizationRequest->getClient()->getRedirectUri()[0]
327
                : $authorizationRequest->getClient()->getRedirectUri()
328
            : $authorizationRequest->getRedirectUri();
329
330
        // The user approved the client, redirect them back with an auth code
331
        if ($authorizationRequest->isAuthorizationApproved() === true) {
332
            $authCode = $this->issueAuthCode(
333
                $this->authCodeTTL,
334
                $authorizationRequest->getClient(),
335
                $authorizationRequest->getUser()->getIdentifier(),
336
                $authorizationRequest->getRedirectUri(),
337
                $authorizationRequest->getScopes()
338
            );
339
340
            $payload = [
341
                'client_id'             => $authCode->getClient()->getIdentifier(),
342
                'redirect_uri'          => $authCode->getRedirectUri(),
343
                'auth_code_id'          => $authCode->getIdentifier(),
344
                'scopes'                => $authCode->getScopes(),
345
                'user_id'               => $authCode->getUserIdentifier(),
346
                'expire_time'           => (new \DateTime())->add($this->authCodeTTL)->format('U'),
347
                'code_challenge'        => $authorizationRequest->getCodeChallenge(),
348
                'code_challenge_method' => $authorizationRequest->getCodeChallengeMethod(),
349
            ];
350
351
            $response = new RedirectResponse();
352
            $response->setRedirectUri(
353
                $this->makeRedirectUri(
354
                    $finalRedirectUri,
355
                    [
356
                        'code'  => $this->encrypt(
357
                            json_encode(
358
                                $payload
359
                            )
360
                        ),
361
                        'state' => $authorizationRequest->getState(),
362
                    ]
363
                )
364
            );
365
366
            return $response;
367
        }
368
369
        // The user denied the client, redirect them back with an error
370
        throw OAuthServerException::accessDenied(
371
            'The user denied the request',
372
            $this->makeRedirectUri(
373
                $finalRedirectUri,
374
                [
375
                    'state' => $authorizationRequest->getState(),
376
                ]
377
            )
378
        );
379
    }
380
}
381