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

AuthCodeGrant::validateAuthCodePayload()   A

Complexity

Conditions 4
Paths 5

Size

Total Lines 19
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 19
rs 9.2
cc 4
eloc 13
nc 5
nop 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
    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 mixed $authCodePayload
187
     *
188
     * @throws OAuthServerException
189
     */
190
    private function validateAuthCodePayload($authCodePayload)
191
    {
192
        if (false === is_object($authCodePayload)) {
193
            throw OAuthServerException::invalidRequest('code');
194
        }
195
196
        $validCodePayload =
197
            isset($authCodePayload->expire_time) &
198
            isset($authCodePayload->auth_code_id) &
199
            isset($authCodePayload->client_id) &
200
            isset($authCodePayload->redirect_uri) &
201
            isset($authCodePayload->scopes) &&
202
            is_array($authCodePayload->scopes) &
203
            isset($authCodePayload->user_id);
204
205
        if (false === $validCodePayload) {
206
            throw OAuthServerException::invalidRequest('code');
207
        }
208
    }
209
210
    /**
211
     * Return the grant identifier that can be used in matching up requests.
212
     *
213
     * @return string
214
     */
215
    public function getIdentifier()
216
    {
217
        return 'authorization_code';
218
    }
219
220
    /**
221
     * {@inheritdoc}
222
     */
223
    public function canRespondToAuthorizationRequest(ServerRequestInterface $request)
224
    {
225
        return (
226
            array_key_exists('response_type', $request->getQueryParams())
227
            && $request->getQueryParams()['response_type'] === 'code'
228
            && isset($request->getQueryParams()['client_id'])
229
        );
230
    }
231
232
    /**
233
     * {@inheritdoc}
234
     */
235
    public function validateAuthorizationRequest(ServerRequestInterface $request)
236
    {
237
        $clientId = $this->getQueryStringParameter(
238
            'client_id',
239
            $request,
240
            $this->getServerParameter('PHP_AUTH_USER', $request)
241
        );
242
        if (is_null($clientId)) {
243
            throw OAuthServerException::invalidRequest('client_id');
244
        }
245
246
        $client = $this->clientRepository->getClientEntity(
247
            $clientId,
248
            $this->getIdentifier(),
249
            null,
250
            false
251
        );
252
253
        if ($client instanceof ClientEntityInterface === false) {
254
            $this->getEmitter()->emit(new RequestEvent(RequestEvent::CLIENT_AUTHENTICATION_FAILED, $request));
255
            throw OAuthServerException::invalidClient();
256
        }
257
258
        $redirectUri = $this->getQueryStringParameter('redirect_uri', $request);
259
        if ($redirectUri !== null) {
260
            if (
261
                is_string($client->getRedirectUri())
262
                && (strcmp($client->getRedirectUri(), $redirectUri) !== 0)
263
            ) {
264
                $this->getEmitter()->emit(new RequestEvent(RequestEvent::CLIENT_AUTHENTICATION_FAILED, $request));
265
                throw OAuthServerException::invalidClient();
266
            } elseif (
267
                is_array($client->getRedirectUri())
268
                && in_array($redirectUri, $client->getRedirectUri()) === false
269
            ) {
270
                $this->getEmitter()->emit(new RequestEvent(RequestEvent::CLIENT_AUTHENTICATION_FAILED, $request));
271
                throw OAuthServerException::invalidClient();
272
            }
273
        }
274
275
        $scopes = $this->validateScopes(
276
            $this->getQueryStringParameter('scope', $request),
277
            is_array($client->getRedirectUri())
278
                ? $client->getRedirectUri()[0]
279
                : $client->getRedirectUri()
280
        );
281
282
        $stateParameter = $this->getQueryStringParameter('state', $request);
283
284
        $authorizationRequest = new AuthorizationRequest();
285
        $authorizationRequest->setGrantTypeId($this->getIdentifier());
286
        $authorizationRequest->setClient($client);
287
        $authorizationRequest->setRedirectUri($redirectUri);
288
        $authorizationRequest->setState($stateParameter);
289
        $authorizationRequest->setScopes($scopes);
290
291
        if ($this->enableCodeExchangeProof === true) {
292
            $codeChallenge = $this->getQueryStringParameter('code_challenge', $request);
293
            if ($codeChallenge === null) {
294
                throw OAuthServerException::invalidRequest('code_challenge');
295
            }
296
297
            if (preg_match('/^[A-Za-z0-9-._~]{43,128}$/', $codeChallenge) !== 1) {
298
                throw OAuthServerException::invalidRequest(
299
                    'code_challenge',
300
                    'The code_challenge must be between 43 and 128 characters'
301
                );
302
            }
303
304
            $codeChallengeMethod = $this->getQueryStringParameter('code_challenge_method', $request, 'plain');
305
            if (in_array($codeChallengeMethod, ['plain', 'S256']) === false) {
306
                throw OAuthServerException::invalidRequest(
307
                    'code_challenge_method',
308
                    'Code challenge method must be `plain` or `S256`'
309
                );
310
            }
311
312
            $authorizationRequest->setCodeChallenge($codeChallenge);
313
            $authorizationRequest->setCodeChallengeMethod($codeChallengeMethod);
314
        }
315
316
        return $authorizationRequest;
317
    }
318
319
    /**
320
     * {@inheritdoc}
321
     */
322
    public function completeAuthorizationRequest(AuthorizationRequest $authorizationRequest)
323
    {
324
        if ($authorizationRequest->getUser() instanceof UserEntityInterface === false) {
325
            throw new \LogicException('An instance of UserEntityInterface should be set on the AuthorizationRequest');
326
        }
327
328
        $finalRedirectUri = ($authorizationRequest->getRedirectUri() === null)
329
            ? is_array($authorizationRequest->getClient()->getRedirectUri())
330
                ? $authorizationRequest->getClient()->getRedirectUri()[0]
331
                : $authorizationRequest->getClient()->getRedirectUri()
332
            : $authorizationRequest->getRedirectUri();
333
334
        // The user approved the client, redirect them back with an auth code
335
        if ($authorizationRequest->isAuthorizationApproved() === true) {
336
            $authCode = $this->issueAuthCode(
337
                $this->authCodeTTL,
338
                $authorizationRequest->getClient(),
339
                $authorizationRequest->getUser()->getIdentifier(),
340
                $authorizationRequest->getRedirectUri(),
341
                $authorizationRequest->getScopes()
342
            );
343
344
            $payload = [
345
                'client_id'             => $authCode->getClient()->getIdentifier(),
346
                'redirect_uri'          => $authCode->getRedirectUri(),
347
                'auth_code_id'          => $authCode->getIdentifier(),
348
                'scopes'                => $authCode->getScopes(),
349
                'user_id'               => $authCode->getUserIdentifier(),
350
                'expire_time'           => (new \DateTime())->add($this->authCodeTTL)->format('U'),
351
                'code_challenge'        => $authorizationRequest->getCodeChallenge(),
352
                'code_challenge_method' => $authorizationRequest->getCodeChallengeMethod(),
353
            ];
354
355
            $response = new RedirectResponse();
356
            $response->setRedirectUri(
357
                $this->makeRedirectUri(
358
                    $finalRedirectUri,
359
                    [
360
                        'code'  => $this->encrypt(
361
                            json_encode(
362
                                $payload
363
                            )
364
                        ),
365
                        'state' => $authorizationRequest->getState(),
366
                    ]
367
                )
368
            );
369
370
            return $response;
371
        }
372
373
        // The user denied the client, redirect them back with an error
374
        throw OAuthServerException::accessDenied(
375
            'The user denied the request',
376
            $this->makeRedirectUri(
377
                $finalRedirectUri,
378
                [
379
                    'state' => $authorizationRequest->getState(),
380
                ]
381
            )
382
        );
383
    }
384
}
385