GitHub Access Token became invalid

It seems like the GitHub access token used for retrieving details about this repository from GitHub became invalid. This might prevent certain types of inspections from being run (in particular, everything related to pull requests).
Please ask an admin of your repository to re-new the access token on this website.

Issues (13)

Security Analysis    not enabled

This project does not seem to handle request data directly as such no vulnerable execution paths were found.

  Cross-Site Scripting
Cross-Site Scripting enables an attacker to inject code into the response of a web-request that is viewed by other users. It can for example be used to bypass access controls, or even to take over other users' accounts.
  File Exposure
File Exposure allows an attacker to gain access to local files that he should not be able to access. These files can for example include database credentials, or other configuration files.
  File Manipulation
File Manipulation enables an attacker to write custom data to files. This potentially leads to injection of arbitrary code on the server.
  Object Injection
Object Injection enables an attacker to inject an object into PHP code, and can lead to arbitrary code execution, file exposure, or file manipulation attacks.
  Code Injection
Code Injection enables an attacker to execute arbitrary code on the server.
  Response Splitting
Response Splitting can be used to send arbitrary responses.
  File Inclusion
File Inclusion enables an attacker to inject custom files into PHP's file loading mechanism, either explicitly passed to include, or for example via PHP's auto-loading mechanism.
  Command Injection
Command Injection enables an attacker to inject a shell command that is execute with the privileges of the web-server. This can be used to expose sensitive data, or gain access of your server.
  SQL Injection
SQL Injection enables an attacker to execute arbitrary SQL code on your database server gaining access to user data, or manipulating user data.
  XPath Injection
XPath Injection enables an attacker to modify the parts of XML document that are read. If that XML document is for example used for authentication, this can lead to further vulnerabilities similar to SQL Injection.
  LDAP Injection
LDAP Injection enables an attacker to inject LDAP statements potentially granting permission to run unauthorized queries, or modify content inside the LDAP tree.
  Header Injection
  Other Vulnerability
This category comprises other attack vectors such as manipulating the PHP runtime, loading custom extensions, freezing the runtime, or similar.
  Regex Injection
Regex Injection enables an attacker to execute arbitrary code in your PHP process.
  XML Injection
XML Injection enables an attacker to read files on your local filesystem including configuration files, or can be abused to freeze your web-server process.
  Variable Injection
Variable Injection enables an attacker to overwrite program variables with custom data, and can lead to further vulnerabilities.
Unfortunately, the security analysis is currently not available for your project. If you are a non-commercial open-source project, please contact support to gain access.

src/OAuthClient.php (4 issues)

Upgrade to new PHP Analysis Engine

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
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.

Loading history...
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.

Loading history...
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.

Loading history...
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.

Loading history...
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