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.
Completed
Push — master ( 454766...08b283 )
by François
02:26
created

OAuthClient::get()   C

Complexity

Conditions 8
Paths 10

Size

Total Lines 67
Code Lines 33

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 67
rs 6.6523
c 0
b 0
f 0
cc 8
eloc 33
nc 10
nop 3

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
/**
3
 *  Copyright (C) 2017 François Kooman <[email protected]>.
4
 *
5
 *  This program is free software: you can redistribute it and/or modify
6
 *  it under the terms of the GNU Affero General Public License as
7
 *  published by the Free Software Foundation, either version 3 of the
8
 *  License, or (at your option) any later version.
9
 *
10
 *  This program is distributed in the hope that it will be useful,
11
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
12
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13
 *  GNU Affero General Public License for more details.
14
 *
15
 *  You should have received a copy of the GNU Affero General Public License
16
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
17
 */
18
19
namespace fkooman\OAuth\Client;
20
21
use DateInterval;
22
use DateTime;
23
use fkooman\OAuth\Client\Exception\OAuthException;
24
use fkooman\OAuth\Client\Exception\OAuthServerException;
25
use fkooman\OAuth\Client\Http\CurlHttpClient;
26
use fkooman\OAuth\Client\Http\HttpClientInterface;
27
use fkooman\OAuth\Client\Http\Response;
28
use InvalidArgumentException;
29
use ParagonIE\ConstantTime\Base64;
30
use Psr\Log\LoggerInterface;
31
use Psr\Log\NullLogger;
32
33
class OAuthClient
34
{
35
    /** @var Provider */
36
    private $provider;
37
38
    /** @var TokenStorageInterface */
39
    private $tokenStorage;
40
41
    /** @var \fkooman\OAuth\Client\Http\HttpClientInterface */
42
    private $httpClient;
43
44
    /** @var RandomInterface */
45
    private $random;
46
47
    /** @var \Psr\Log\LoggerInterface */
48
    private $logger;
49
50
    /** @var \DateTime */
51
    private $dateTime;
52
53
    /** @var string|null */
54
    private $userId = null;
55
56
    /**
57
     * Instantiate an OAuth 2.0 Client.
58
     *
59
     * @param Provider                      $provider
60
     * @param TokenStorageInterface         $tokenStorage
61
     * @param Http\HttpClientInterface|null $httpClient
62
     * @param RandomInterface|null          $random
63
     * @param DateTime|null                 $dateTime
64
     */
65
    public function __construct(Provider $provider, TokenStorageInterface $tokenStorage, HttpClientInterface $httpClient = null, RandomInterface $random = null, LoggerInterface $logger, DateTime $dateTime = null)
66
    {
67
        $this->provider = $provider;
68
        $this->tokenStorage = $tokenStorage;
69
        if (is_null($httpClient)) {
70
            $httpClient = new CurlHttpClient();
71
        }
72
        $this->httpClient = $httpClient;
73
        if (is_null($random)) {
74
            $random = new Random();
75
        }
76
        $this->random = $random;
77
        if (is_null($logger)) {
78
            $logger = new NullLogger();
79
        }
80
        $this->logger = $logger;
81
        if (is_null($dateTime)) {
82
            $dateTime = new DateTime();
83
        }
84
        $this->dateTime = $dateTime;
85
    }
86
87
    /**
88
     * @param string $userId
89
     */
90
    public function setUserId($userId)
91
    {
92
        $this->userId = $userId;
93
    }
94
95
    public function get($requestScope, $requestUri, array $requestHeaders = [])
0 ignored issues
show
Unused Code introduced by
The parameter $requestScope is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
96
    {
97
        if (is_null($this->userId)) {
98
            throw new OAuthException('userId not set');
99
        }
100
101
        // make sure we have an access token
102
        if (false === $accessToken = $this->tokenStorage->getAccessToken($this->userId)) {
103
            $this->logger->info('no access_token available');
104
105
            return false;
106
        }
107
108
        $refreshedToken = false;
109
        if ($accessToken->isExpired($this->dateTime)) {
110
            $this->logger->info('access_token expired');
111
            // access_token is expired, try to refresh it
112
            if (is_null($accessToken->getRefreshToken())) {
113
                $this->logger->info('no refresh_token available, delete access_token');
114
                // we do not have a refresh_token, delete this access token, it
115
                // is useless now...
116
                $this->tokenStorage->deleteAccessToken($this->userId, $accessToken);
117
118
                return false;
119
            }
120
121
            $this->logger->info('attempting to refresh access_token');
122
            // deal with possibly revoked authorization! XXX
123
            try {
124
                $accessToken = $this->refreshAccessToken($accessToken);
125
            } catch (OAuthServerException $e) {
126
                $this->logger->info(sprintf('unable to use refresh_token %s', $e->getMessage()));
127
128
                // delete the access_token, the refresh_token could not be used
129
                $this->tokenStorage->deleteAccessToken($this->userId, $accessToken);
130
131
                return false;
132
            }
133
134
            // maybe delete old accesstoken here? XXX
135
            $this->logger->info('access_token refreshed');
136
            $refreshedToken = true;
137
        }
138
139
        // add Authorization header to the request headers
140
        $requestHeaders['Authorization'] = sprintf('Bearer %s', $accessToken->getToken());
141
142
        $response = $this->httpClient->get($requestUri, $requestHeaders);
143
        if (401 === $response->getStatusCode()) {
144
            $this->logger->info('access_token appears to be invalid, delete access_token');
145
            // this indicates an invalid access_token
146
            $this->tokenStorage->deleteAccessToken($this->userId, $accessToken);
147
148
            return false;
149
        }
150
151
        $this->logger->info('access_token was valid, call succeeded');
152
153
        if ($refreshedToken) {
154
            $this->logger->info('access_token was refreshed and it worked, so store it now for future use');
155
            // if we refreshed the token, and it was successful, i.e. not a 401,
156
            // update the stored AccessToken
157
            $this->tokenStorage->setAccessToken($this->userId, $accessToken);
158
        }
159
160
        return $response;
161
    }
162
163
    /**
164
     * Obtain an authorization request URL to start the authorization process
165
     * at the OAuth provider.
166
     *
167
     * @param string $scope       the space separated scope tokens
168
     * @param string $redirectUri the URL to redirect back to after coming back
169
     *                            from the OAuth provider (callback URL)
170
     *
171
     * @return string the authorization request URL
172
     *
173
     * @see https://tools.ietf.org/html/rfc6749#section-3.3
174
     * @see https://tools.ietf.org/html/rfc6749#section-3.1.2
175
     */
176
    public function getAuthorizeUri($scope, $redirectUri)
177
    {
178
        $queryParams = http_build_query(
179
            [
180
                'client_id' => $this->provider->getId(),
181
                'redirect_uri' => $redirectUri,
182
                'scope' => $scope,
183
                'state' => $this->random->get(16),
184
                'response_type' => 'code',
185
            ],
186
            '&'
187
        );
188
189
        return sprintf(
190
            '%s%s%s',
191
            $this->provider->getAuthorizationEndpoint(),
192
            false === strpos($this->provider->getAuthorizationEndpoint(), '?') ? '?' : '&',
193
            $queryParams
194
        );
195
    }
196
197
    /**
198
     * Obtain the access token from the OAuth provider after returning from the
199
     * OAuth provider on the redirectUri (callback URL).
200
     *
201
     * @param string $requestUri    the original authorization
202
     *                              request URL as obtained by getAuthorzationRequestUri
203
     * @param string $responseCode  the code passed to the 'code'
204
     *                              query parameter on the callback URL
205
     * @param string $responseState the state passed to the 'state'
206
     *                              query parameter on the callback URL
207
     *
208
     * @return AccessToken
209
     */
210
    public function handleCallback($requestUri, $responseCode, $responseState)
211
    {
212
        if (is_null($this->userId)) {
213
            throw new OAuthException('userId not set');
214
        }
215
216
        // the requestUri parameter is provided by the caller of this call, and
217
        // does NOT contain external input so does not need to be validated
218
        $requestParameters = self::parseRequestUri($requestUri);
219
        if ($responseState !== $requestParameters['state']) {
220
            // the OAuth state from the initial request MUST be the same as the
221
            // state used by the response
222
            throw new OAuthException('invalid OAuth state');
223
        }
224
225
        if ($requestParameters['client_id'] !== $this->provider->getId()) {
226
            // the client_id used for the initial request differs from the
227
            // currently configured Provider, the client_id MUST be identical
228
            throw new OAuthException('unexpected client identifier');
229
        }
230
231
        // prepare access_token request
232
        $tokenRequestData = [
233
            'client_id' => $this->provider->getId(),
234
            'grant_type' => 'authorization_code',
235
            'code' => $responseCode,
236
            'redirect_uri' => $requestParameters['redirect_uri'],
237
        ];
238
239
        $responseData = $this->validateTokenResponse(
240
            $this->httpClient->post(
241
                $this->provider->getTokenEndpoint(),
242
                $tokenRequestData,
243
                [
244
                    'Authorization' => sprintf(
245
                        'Basic %s',
246
                        Base64::encode(
247
                            sprintf('%s:%s', $this->provider->getId(), $this->provider->getSecret())
248
                        )
249
                    ),
250
                ]
251
            ),
252
            $requestParameters['scope']
253
        );
254
255
        $this->tokenStorage->setAccessToken(
256
            $this->userId,
257
            new AccessToken(
258
                $responseData['access_token'],
259
                $responseData['token_type'],
260
                $responseData['scope'],
261
                array_key_exists('refresh_token', $responseData) ? $responseData['refresh_token'] : null,
262
                $responseData['expires_at']
263
            )
264
        );
265
    }
266
267
    /**
268
     * Refresh the access token from the OAuth.
269
     *
270
     * @param string $refreshToken the refresh token
0 ignored issues
show
Bug introduced by
There is no parameter named $refreshToken. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
271
     * @param string $requestScope the scope associated with the previously
0 ignored issues
show
Bug introduced by
There is no parameter named $requestScope. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
272
     *                             obtained access token
273
     *
274
     * @return AccessToken
275
     */
276
    private function refreshAccessToken(AccessToken $accessToken)
277
    {
278
        // prepare access_token request
279
        $tokenRequestData = [
280
            'grant_type' => 'refresh_token',
281
            'refresh_token' => $accessToken->getRefreshToken(),
282
            'scope' => $accessToken->getScope(),
283
        ];
284
285
        $responseData = $this->validateTokenResponse(
286
            $this->httpClient->post(
287
                $this->provider->getTokenEndpoint(),
288
                $tokenRequestData,
289
                [
290
                    'Authorization' => sprintf(
291
                        'Basic %s',
292
                        Base64::encode(
293
                            sprintf('%s:%s', $this->provider->getId(), $this->provider->getSecret())
294
                        )
295
                    ),
296
                ]
297
            ),
298
            $accessToken->getScope()
299
        );
300
301
        return new AccessToken(
302
            $responseData['access_token'],
303
            $responseData['token_type'],
304
            $responseData['scope'],
305
            // if a new refresh_token was provided use that, if not reuse the old one
306
            array_key_exists('refresh_token', $responseData) ? $responseData['refresh_token'] : $accessToken->getRefreshToken(),
307
            $responseData['expires_at']
308
        );
309
    }
310
311
    /**
312
     * Validate the provided URI to see if it has the right format, it is
313
     * provided by the API consumer.
314
     */
315
    private static function parseRequestUri($requestUri)
316
    {
317
        if (!is_string($requestUri)) {
318
            throw new InvalidArgumentException('"requestUri" MUST be string');
319
        }
320
321
        if (false === strpos($requestUri, '?')) {
322
            throw new OAuthException('"requestUri" not valid, no query string');
323
        }
324
325
        parse_str(explode('?', $requestUri)[1], $requestParameters);
326
327
        $requiredParameters = [
328
            'client_id',
329
            'redirect_uri',
330
            'scope',
331
            'state',
332
            'response_type',
333
        ];
334
335
        // all of the above parameters were part of the requestUri, make sure
336
        // they are still there...
337 View Code Duplication
        foreach ($requiredParameters as $requiredParameter) {
0 ignored issues
show
Duplication introduced by
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...
338
            if (!array_key_exists($requiredParameter, $requestParameters)) {
339
                throw new OAuthException(
340
                    sprintf(
341
                        'request URI not valid, missing required query parameter "%s"',
342
                        $requiredParameter
343
                    )
344
                );
345
            }
346
        }
347
348
        return $requestParameters;
349
    }
350
351
    private function validateTokenResponse(Response $response, $requestScope)
352
    {
353
        $tokenResponse = $response->json();
354
        // XXX what if not array?
355
356
        // check if an error occurred
357
        if (array_key_exists('error', $tokenResponse)) {
358
            if (array_key_exists('error_description', $tokenResponse)) {
359
                throw new OAuthServerException(sprintf('%s: %s', $tokenResponse['error'], $tokenResponse['error_description']));
360
            }
361
362
            throw new OAuthServerException($tokenResponse['error']);
363
        }
364
365
        $requiredParameters = [
366
            'access_token',
367
            'token_type',
368
        ];
369
370 View Code Duplication
        foreach ($requiredParameters as $requiredParameter) {
0 ignored issues
show
Duplication introduced by
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...
371
            if (!array_key_exists($requiredParameter, $tokenResponse)) {
372
                throw new OAuthException(
373
                    sprintf(
374
                        'token response not valid, missing required parameter "%s"',
375
                        $requiredParameter
376
                    )
377
                );
378
            }
379
        }
380
381
        if (!array_key_exists('scope', $tokenResponse)) {
382
            // if the token endpoint does not return a 'scope' value, the
383
            // specification says the requested scope was granted
384
            $tokenResponse['scope'] = $requestScope;
385
        }
386
387
        $tokenResponse['expires_at'] = $this->calculateExpiresAt($tokenResponse);
388
389
        return $tokenResponse;
390
    }
391
392
    private function calculateExpiresAt(array $tokenResponse)
393
    {
394
        $dateTime = clone $this->dateTime;
395
        if (array_key_exists('expires_in', $tokenResponse)) {
396
            return date_add($dateTime, new DateInterval(sprintf('PT%dS', $tokenResponse['expires_in'])));
397
        }
398
399
        // if the 'expires_in' field is not available, we default to 1 year
400
        return date_add($dateTime, new DateInterval('P1Y'));
401
    }
402
}
403