This project does not seem to handle request data directly as such no vulnerable execution paths were found.
include
, or for example
via PHP's auto-loading mechanism.
These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more
1 | <?php |
||
2 | |||
3 | /** |
||
4 | * Copyright (c) 2016, 2017 François Kooman <[email protected]>. |
||
5 | * |
||
6 | * Permission is hereby granted, free of charge, to any person obtaining a copy |
||
7 | * of this software and associated documentation files (the "Software"), to deal |
||
8 | * in the Software without restriction, including without limitation the rights |
||
9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
||
10 | * copies of the Software, and to permit persons to whom the Software is |
||
11 | * furnished to do so, subject to the following conditions: |
||
12 | * |
||
13 | * The above copyright notice and this permission notice shall be included in all |
||
14 | * copies or substantial portions of the Software. |
||
15 | * |
||
16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
||
17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
||
18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
||
19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
||
20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
||
21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
||
22 | * SOFTWARE. |
||
23 | */ |
||
24 | |||
25 | namespace fkooman\OAuth\Client; |
||
26 | |||
27 | use DateTime; |
||
28 | use fkooman\OAuth\Client\Exception\OAuthException; |
||
29 | use fkooman\OAuth\Client\Exception\OAuthServerException; |
||
30 | use fkooman\OAuth\Client\Http\HttpClientInterface; |
||
31 | use fkooman\OAuth\Client\Http\Request; |
||
32 | use fkooman\OAuth\Client\Http\Response; |
||
33 | use ParagonIE\ConstantTime\Base64; |
||
34 | use ParagonIE\ConstantTime\Base64UrlSafe; |
||
35 | |||
36 | class OAuthClient |
||
37 | { |
||
38 | /** @var TokenStorageInterface */ |
||
39 | private $tokenStorage; |
||
40 | |||
41 | /** @var \fkooman\OAuth\Client\Http\HttpClientInterface */ |
||
42 | private $httpClient; |
||
43 | |||
44 | /** @var SessionInterface */ |
||
45 | private $session; |
||
46 | |||
47 | /** @var RandomInterface */ |
||
48 | private $random; |
||
49 | |||
50 | /** @var \DateTime */ |
||
51 | private $dateTime; |
||
52 | |||
53 | /** @var Provider */ |
||
54 | private $provider = null; |
||
55 | |||
56 | /** @var string */ |
||
57 | private $userId = null; |
||
58 | |||
59 | /** |
||
60 | * @param TokenStorageInterface $tokenStorage |
||
61 | * @param Http\HttpClientInterface $httpClient |
||
62 | * @param SessionInterface|null $session |
||
63 | * @param RandomInterface|null $random |
||
64 | * @param \DateTime|null $dateTime |
||
65 | */ |
||
66 | public function __construct( |
||
67 | TokenStorageInterface $tokenStorage, |
||
68 | HttpClientInterface $httpClient, |
||
69 | SessionInterface $session = null, |
||
70 | RandomInterface $random = null, |
||
71 | DateTime $dateTime = null |
||
72 | ) { |
||
73 | $this->tokenStorage = $tokenStorage; |
||
74 | $this->httpClient = $httpClient; |
||
75 | if (is_null($session)) { |
||
76 | $session = new Session(); |
||
77 | } |
||
78 | $this->session = $session; |
||
79 | if (is_null($random)) { |
||
80 | $random = new Random(); |
||
81 | } |
||
82 | $this->random = $random; |
||
83 | if (is_null($dateTime)) { |
||
84 | $dateTime = new DateTime(); |
||
85 | } |
||
86 | $this->dateTime = $dateTime; |
||
87 | } |
||
88 | |||
89 | /** |
||
90 | * @param \DateTime $dateTime |
||
91 | */ |
||
92 | public function setDateTime(DateTime $dateTime) |
||
93 | { |
||
94 | $this->dateTime = $dateTime; |
||
95 | } |
||
96 | |||
97 | /** |
||
98 | * @param Provider $provider |
||
99 | */ |
||
100 | public function setProvider(Provider $provider) |
||
101 | { |
||
102 | $this->provider = $provider; |
||
103 | } |
||
104 | |||
105 | /** |
||
106 | * @param string $userId |
||
107 | */ |
||
108 | public function setUserId($userId) |
||
109 | { |
||
110 | $this->userId = $userId; |
||
111 | } |
||
112 | |||
113 | /** |
||
114 | * Perform a GET request, convenience wrapper for ::send(). |
||
115 | * |
||
116 | * @param string $requestScope |
||
117 | * @param string $requestUri |
||
118 | * @param array $requestHeaders |
||
119 | * |
||
120 | * @return Http\Response|false |
||
121 | */ |
||
122 | public function get($requestScope, $requestUri, array $requestHeaders = []) |
||
123 | { |
||
124 | return $this->send($requestScope, Request::get($requestUri, $requestHeaders)); |
||
125 | } |
||
126 | |||
127 | /** |
||
128 | * Perform a POST request, convenience wrapper for ::send(). |
||
129 | * |
||
130 | * @param string $requestScope |
||
131 | * @param string $requestUri |
||
132 | * @param array $postBody |
||
133 | * @param array $requestHeaders |
||
134 | * |
||
135 | * @return Http\Response|false |
||
136 | */ |
||
137 | public function post($requestScope, $requestUri, array $postBody, array $requestHeaders = []) |
||
138 | { |
||
139 | return $this->send($requestScope, Request::post($requestUri, $postBody, $requestHeaders)); |
||
140 | } |
||
141 | |||
142 | /** |
||
143 | * Perform a HTTP request. |
||
144 | * |
||
145 | * @param string $requestScope |
||
146 | * @param Http\Request $request |
||
147 | * |
||
148 | * @return Response|false |
||
149 | */ |
||
150 | public function send($requestScope, Request $request) |
||
151 | { |
||
152 | if (is_null($this->userId)) { |
||
153 | throw new OAuthException('userId not set'); |
||
154 | } |
||
155 | |||
156 | if (false === $accessToken = $this->getAccessToken($requestScope)) { |
||
157 | return false; |
||
158 | } |
||
159 | |||
160 | if ($accessToken->isExpired($this->dateTime)) { |
||
161 | // access_token is expired, try to refresh it |
||
162 | if (is_null($accessToken->getRefreshToken())) { |
||
163 | // we do not have a refresh_token, delete this access token, it |
||
164 | // is useless now... |
||
165 | $this->tokenStorage->deleteAccessToken($this->userId, $accessToken); |
||
166 | |||
167 | return false; |
||
168 | } |
||
169 | |||
170 | // try to refresh the AccessToken |
||
171 | if (false === $accessToken = $this->refreshAccessToken($accessToken)) { |
||
172 | // didn't work |
||
173 | return false; |
||
174 | } |
||
175 | } |
||
176 | |||
177 | // add Authorization header to the request headers |
||
178 | $request->setHeader('Authorization', sprintf('Bearer %s', $accessToken->getToken())); |
||
179 | |||
180 | $response = $this->httpClient->send($request); |
||
181 | if (401 === $response->getStatusCode()) { |
||
182 | // the access_token was not accepted, but isn't expired, we assume |
||
183 | // the user revoked it, also no need to try with refresh_token |
||
184 | $this->tokenStorage->deleteAccessToken($this->userId, $accessToken); |
||
185 | |||
186 | return false; |
||
187 | } |
||
188 | |||
189 | return $response; |
||
190 | } |
||
191 | |||
192 | /** |
||
193 | * Obtain an authorization request URL to start the authorization process |
||
194 | * at the OAuth provider. |
||
195 | * |
||
196 | * @param string $scope the space separated scope tokens |
||
197 | * @param string $redirectUri the URL registered at the OAuth provider, to |
||
198 | * be redirected back to |
||
199 | * |
||
200 | * @return string the authorization request URL |
||
201 | * |
||
202 | * @see https://tools.ietf.org/html/rfc6749#section-3.3 |
||
203 | * @see https://tools.ietf.org/html/rfc6749#section-3.1.2 |
||
204 | */ |
||
205 | public function getAuthorizeUri($scope, $redirectUri) |
||
206 | { |
||
207 | if (is_null($this->userId)) { |
||
208 | throw new OAuthException('userId not set'); |
||
209 | } |
||
210 | |||
211 | $codeVerifier = $this->generateCodeVerifier(); |
||
212 | |||
213 | $queryParameters = [ |
||
214 | 'client_id' => $this->provider->getClientId(), |
||
215 | 'redirect_uri' => $redirectUri, |
||
216 | 'scope' => $scope, |
||
217 | 'state' => $this->random->get(16), |
||
218 | 'response_type' => 'code', |
||
219 | 'code_challenge_method' => 'S256', |
||
220 | 'code_challenge' => self::hashCodeVerifier($codeVerifier), |
||
221 | ]; |
||
222 | |||
223 | $authorizeUri = sprintf( |
||
224 | '%s%s%s', |
||
225 | $this->provider->getAuthorizationEndpoint(), |
||
226 | false === strpos($this->provider->getAuthorizationEndpoint(), '?') ? '?' : '&', |
||
227 | http_build_query($queryParameters, '&') |
||
228 | ); |
||
229 | $this->session->set( |
||
230 | '_oauth2_session', |
||
231 | array_merge( |
||
232 | $queryParameters, |
||
233 | [ |
||
234 | 'code_verifier' => $codeVerifier, |
||
235 | 'user_id' => $this->userId, |
||
236 | 'provider_id' => $this->provider->getProviderId(), |
||
237 | ] |
||
238 | ) |
||
239 | ); |
||
240 | |||
241 | return $authorizeUri; |
||
242 | } |
||
243 | |||
244 | /** |
||
245 | * @param string $responseCode the code passed to the "code" |
||
246 | * query parameter on the callback URL |
||
247 | * @param string $responseState the state passed to the "state" |
||
248 | * query parameter on the callback URL |
||
249 | */ |
||
250 | public function handleCallback($responseCode, $responseState) |
||
251 | { |
||
252 | if (is_null($this->userId)) { |
||
253 | throw new OAuthException('userId not set'); |
||
254 | } |
||
255 | |||
256 | // get and delete the OAuth session information |
||
257 | $sessionData = $this->session->take('_oauth2_session'); |
||
258 | |||
259 | if (!hash_equals($sessionData['state'], $responseState)) { |
||
260 | // the OAuth state from the initial request MUST be the same as the |
||
261 | // state used by the response |
||
262 | throw new OAuthException('invalid session (state)'); |
||
263 | } |
||
264 | |||
265 | // session providerId MUST match current set Provider |
||
266 | if ($sessionData['provider_id'] !== $this->provider->getProviderId()) { |
||
267 | throw new OAuthException('invalid session (provider_id)'); |
||
268 | } |
||
269 | |||
270 | // session userId MUST match current set userId |
||
271 | if ($sessionData['user_id'] !== $this->userId) { |
||
272 | throw new OAuthException('invalid session (user_id)'); |
||
273 | } |
||
274 | |||
275 | // prepare access_token request |
||
276 | $tokenRequestData = [ |
||
277 | 'client_id' => $this->provider->getClientId(), |
||
278 | 'grant_type' => 'authorization_code', |
||
279 | 'code' => $responseCode, |
||
280 | 'redirect_uri' => $sessionData['redirect_uri'], |
||
281 | 'code_verifier' => $sessionData['code_verifier'], |
||
282 | ]; |
||
283 | |||
284 | $requestHeaders = []; |
||
285 | // if we have a secret registered for the client, use it |
||
286 | View Code Duplication | if (!is_null($this->provider->getSecret())) { |
|
0 ignored issues
–
show
|
|||
287 | $requestHeaders = [ |
||
288 | 'Authorization' => sprintf( |
||
289 | 'Basic %s', |
||
290 | Base64::encode( |
||
291 | sprintf('%s:%s', $this->provider->getClientId(), $this->provider->getSecret()) |
||
292 | ) |
||
293 | ), |
||
294 | ]; |
||
295 | } |
||
296 | |||
297 | $response = $this->httpClient->send( |
||
298 | Request::post( |
||
299 | $this->provider->getTokenEndpoint(), |
||
300 | $tokenRequestData, |
||
301 | $requestHeaders |
||
302 | ) |
||
303 | ); |
||
304 | |||
305 | if (400 === $response->getStatusCode()) { |
||
306 | // check for "invalid_grant" |
||
307 | $responseData = $response->json(); |
||
308 | View Code Duplication | if (!array_key_exists('error', $responseData) || 'invalid_grant' !== $responseData['error']) { |
|
0 ignored issues
–
show
This code seems to be duplicated across your project.
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation. You can also find more detailed suggestions in the “Code” section of your repository. ![]() |
|||
309 | // not an "invalid_grant", we can't deal with this here... |
||
310 | throw new OAuthServerException($response); |
||
311 | } |
||
312 | |||
313 | throw new OAuthException('authorization_code was not accepted by the server'); |
||
314 | } |
||
315 | |||
316 | if (!$response->isOkay()) { |
||
317 | // if there is any other error, we can't deal with this here... |
||
318 | throw new OAuthServerException($response); |
||
319 | } |
||
320 | |||
321 | $this->tokenStorage->storeAccessToken( |
||
322 | $this->userId, |
||
323 | AccessToken::fromCodeResponse( |
||
324 | $this->provider, |
||
325 | $this->dateTime, |
||
326 | $response->json(), |
||
327 | // in case server does not return a scope, we know it granted |
||
328 | // our requested scope |
||
329 | $sessionData['scope'] |
||
330 | ) |
||
331 | ); |
||
332 | } |
||
333 | |||
334 | /** |
||
335 | * Verify if an AccessToken in the list that matches this scope, bound to |
||
336 | * providerId and userId. |
||
337 | * |
||
338 | * This method has NO side effects, i.e. it will not try to use, refresh or |
||
339 | * delete AccessTokens. If a token is expired, but a refresh token is |
||
340 | * available it is assumed that an AccessToken is available. |
||
341 | * |
||
342 | * NOTE: this does not mean that the token will also be accepted by the |
||
343 | * resource server! |
||
344 | * |
||
345 | * @param string $scope |
||
346 | * |
||
347 | * @return bool |
||
348 | */ |
||
349 | public function hasAccessToken($scope) |
||
350 | { |
||
351 | if (false === $accessToken = $this->getAccessToken($scope)) { |
||
352 | return false; |
||
353 | } |
||
354 | |||
355 | // is it expired? but do we have a refresh_token? |
||
356 | if ($accessToken->isExpired($this->dateTime)) { |
||
357 | // access_token is expired |
||
358 | if (!is_null($accessToken->getRefreshToken())) { |
||
359 | // but we have a refresh_token |
||
360 | return true; |
||
361 | } |
||
362 | |||
363 | // no refresh_token |
||
364 | return false; |
||
365 | } |
||
366 | |||
367 | // not expired |
||
368 | return true; |
||
369 | } |
||
370 | |||
371 | /** |
||
372 | * @param AccessToken $accessToken |
||
373 | * |
||
374 | * @return AccessToken|false |
||
375 | */ |
||
376 | private function refreshAccessToken(AccessToken $accessToken) |
||
377 | { |
||
378 | // prepare access_token request |
||
379 | $tokenRequestData = [ |
||
380 | 'grant_type' => 'refresh_token', |
||
381 | 'refresh_token' => $accessToken->getRefreshToken(), |
||
382 | 'scope' => $accessToken->getScope(), |
||
383 | ]; |
||
384 | |||
385 | $requestHeaders = []; |
||
386 | // if we have a secret registered for the client, use it |
||
387 | View Code Duplication | if (!is_null($this->provider->getSecret())) { |
|
0 ignored issues
–
show
This code seems to be duplicated across your project.
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation. You can also find more detailed suggestions in the “Code” section of your repository. ![]() |
|||
388 | $requestHeaders = [ |
||
389 | 'Authorization' => sprintf( |
||
390 | 'Basic %s', |
||
391 | Base64::encode( |
||
392 | sprintf('%s:%s', $this->provider->getClientId(), $this->provider->getSecret()) |
||
393 | ) |
||
394 | ), |
||
395 | ]; |
||
396 | } |
||
397 | |||
398 | $response = $this->httpClient->send( |
||
399 | Request::post( |
||
400 | $this->provider->getTokenEndpoint(), |
||
401 | $tokenRequestData, |
||
402 | $requestHeaders |
||
403 | ) |
||
404 | ); |
||
405 | |||
406 | if (400 === $response->getStatusCode()) { |
||
407 | // check for "invalid_grant" |
||
408 | $responseData = $response->json(); |
||
409 | View Code Duplication | if (!array_key_exists('error', $responseData) || 'invalid_grant' !== $responseData['error']) { |
|
0 ignored issues
–
show
This code seems to be duplicated across your project.
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation. You can also find more detailed suggestions in the “Code” section of your repository. ![]() |
|||
410 | // not an "invalid_grant", we can't deal with this here... |
||
411 | throw new OAuthServerException($response); |
||
412 | } |
||
413 | |||
414 | // delete the access_token, we assume the user revoked it |
||
415 | $this->tokenStorage->deleteAccessToken($this->userId, $accessToken); |
||
416 | |||
417 | return false; |
||
418 | } |
||
419 | |||
420 | if (!$response->isOkay()) { |
||
421 | // if there is any other error, we can't deal with this here... |
||
422 | throw new OAuthServerException($response); |
||
423 | } |
||
424 | |||
425 | $accessToken = AccessToken::fromRefreshResponse( |
||
426 | $this->provider, |
||
427 | $this->dateTime, |
||
428 | $response->json(), |
||
429 | // provide the old AccessToken to borrow some fields if the server |
||
430 | // does not provide them on "refresh" |
||
431 | $accessToken |
||
432 | ); |
||
433 | |||
434 | // store the refreshed AccessToken |
||
435 | $this->tokenStorage->storeAccessToken($this->userId, $accessToken); |
||
436 | |||
437 | return $accessToken; |
||
438 | } |
||
439 | |||
440 | /** |
||
441 | * Find an AccessToken in the list that matches this scope, bound to |
||
442 | * providerId and userId. |
||
443 | * |
||
444 | * @param string $scope |
||
445 | * |
||
446 | * @return AccessToken|false |
||
447 | */ |
||
448 | private function getAccessToken($scope) |
||
449 | { |
||
450 | $accessTokenList = $this->tokenStorage->getAccessTokenList($this->userId); |
||
451 | foreach ($accessTokenList as $accessToken) { |
||
452 | if ($this->provider->getProviderId() !== $accessToken->getProviderId()) { |
||
453 | continue; |
||
454 | } |
||
455 | if ($scope !== $accessToken->getScope()) { |
||
456 | continue; |
||
457 | } |
||
458 | |||
459 | return $accessToken; |
||
460 | } |
||
461 | |||
462 | return false; |
||
463 | } |
||
464 | |||
465 | /** |
||
466 | * @param string $codeVerifier |
||
467 | * |
||
468 | * @return string |
||
469 | */ |
||
470 | private static function hashCodeVerifier($codeVerifier) |
||
471 | { |
||
472 | return rtrim( |
||
473 | Base64UrlSafe::encode( |
||
474 | hash( |
||
475 | 'sha256', |
||
476 | $codeVerifier, |
||
477 | true |
||
478 | ) |
||
479 | ), |
||
480 | '=' |
||
481 | ); |
||
482 | } |
||
483 | |||
484 | /** |
||
485 | * @return string |
||
486 | */ |
||
487 | private function generateCodeVerifier() |
||
488 | { |
||
489 | return rtrim( |
||
490 | Base64UrlSafe::encode( |
||
491 | $this->random->get(32, true) |
||
492 | ), |
||
493 | '=' |
||
494 | ); |
||
495 | } |
||
496 | } |
||
497 |
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.