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 ( 487882...513f7c )
by François
13:06
created

OAuth2Client::handleCallback()   B

Complexity

Conditions 5
Paths 4

Size

Total Lines 56
Code Lines 30

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 56
rs 8.7592
c 0
b 0
f 0
cc 5
eloc 30
nc 4
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
/**
34
 * OAuth 2.0 Client. Helper class to make it easy to obtain an access token
35
 * from an OAuth 2.0 provider.
36
 */
37
class OAuth2Client
38
{
39
    /** @var Provider */
40
    private $provider;
41
42
    /** @var TokenStorageInterface */
43
    private $tokenStorage;
44
45
    /** @var \fkooman\OAuth\Client\Http\HttpClientInterface */
46
    private $httpClient;
47
48
    /** @var RandomInterface */
49
    private $random;
50
51
    /** @var \Psr\Log\LoggerInterface */
52
    private $logger;
53
54
    /** @var \DateTime */
55
    private $dateTime;
56
57
    /** @var string|null */
58
    private $userId = null;
59
60
    /**
61
     * Instantiate an OAuth 2.0 Client.
62
     *
63
     * @param Provider                      $provider
64
     * @param TokenStorageInterface         $tokenStorage
65
     * @param Http\HttpClientInterface|null $httpClient
66
     * @param RandomInterface|null          $random
67
     * @param DateTime|null                 $dateTime
68
     */
69
    public function __construct(Provider $provider, TokenStorageInterface $tokenStorage, HttpClientInterface $httpClient = null, RandomInterface $random = null, LoggerInterface $logger, DateTime $dateTime = null)
70
    {
71
        $this->provider = $provider;
72
        $this->tokenStorage = $tokenStorage;
73
        if (is_null($httpClient)) {
74
            $httpClient = new CurlHttpClient();
75
        }
76
        $this->httpClient = $httpClient;
77
        if (is_null($random)) {
78
            $random = new Random();
79
        }
80
        $this->random = $random;
81
        if (is_null($logger)) {
82
            $logger = new NullLogger();
83
        }
84
        $this->logger = $logger;
85
        if (is_null($dateTime)) {
86
            $dateTime = new DateTime();
87
        }
88
        $this->dateTime = $dateTime;
89
    }
90
91
    /**
92
     * @param string $userId
93
     */
94
    public function setUserId($userId)
95
    {
96
        $this->userId = $userId;
97
    }
98
99
    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...
100
    {
101
        if (is_null($this->userId)) {
102
            throw new OAuthException('userId not set');
103
        }
104
105
        // make sure we have an access token
106
        if (false === $accessToken = $this->tokenStorage->getAccessToken($this->userId)) {
107
            $this->logger->info('no access_token available');
108
109
            return false;
110
        }
111
112
        $refreshedToken = false;
113
        if ($accessToken->isExpired($this->dateTime)) {
114
            $this->logger->info('access_token expired');
115
            // access_token is expired, try to refresh it
116
            if (is_null($accessToken->getRefreshToken())) {
117
                $this->logger->info('no refresh_token available, delete access_token');
118
                // we do not have a refresh_token, delete this access token, it
119
                // is useless now...
120
                $this->tokenStorage->deleteAccessToken($this->userId, $accessToken);
121
122
                return false;
123
            }
124
125
            $this->logger->info('attempting to refresh access_token');
126
            // deal with possibly revoked authorization! XXX
127
            try {
128
                $accessToken = $this->refreshAccessToken($accessToken);
129
            } catch (OAuthServerException $e) {
130
                $this->logger->info(sprintf('unable to use refresh_token %s', $e->getMessage()));
131
132
                // delete the access_token, the refresh_token could not be used
133
                $this->tokenStorage->deleteAccessToken($this->userId, $accessToken);
134
135
                return false;
136
            }
137
138
            // maybe delete old accesstoken here? XXX
139
            $this->logger->info('access_token refreshed');
140
            $refreshedToken = true;
141
        }
142
143
        // add Authorization header to the request headers
144
        $requestHeaders['Authorization'] = sprintf('Bearer %s', $accessToken->getToken());
145
146
        $response = $this->httpClient->get($requestUri, $requestHeaders);
147
        if (401 === $response->getStatusCode()) {
148
            $this->logger->info('access_token appears to be invalid, delete access_token');
149
            // this indicates an invalid access_token
150
            $this->tokenStorage->deleteAccessToken($this->userId, $accessToken);
151
152
            return false;
153
        }
154
155
        $this->logger->info('access_token was valid, call succeeded');
156
157
        if ($refreshedToken) {
158
            $this->logger->info('access_token was refreshed and it worked, so store it now for future use');
159
            // if we refreshed the token, and it was successful, i.e. not a 401,
160
            // update the stored AccessToken
161
            $this->tokenStorage->setAccessToken($this->userId, $accessToken);
162
        }
163
164
        return $response;
165
    }
166
167
    /**
168
     * Obtain an authorization request URL to start the authorization process
169
     * at the OAuth provider.
170
     *
171
     * @param string $scope       the space separated scope tokens
172
     * @param string $redirectUri the URL to redirect back to after coming back
173
     *                            from the OAuth provider (callback URL)
174
     *
175
     * @return string the authorization request URL
176
     *
177
     * @see https://tools.ietf.org/html/rfc6749#section-3.3
178
     * @see https://tools.ietf.org/html/rfc6749#section-3.1.2
179
     */
180
    public function getAuthorizeUri($scope, $redirectUri)
181
    {
182
        $queryParams = http_build_query(
183
            [
184
                'client_id' => $this->provider->getId(),
185
                'redirect_uri' => $redirectUri,
186
                'scope' => $scope,
187
                'state' => $this->random->get(16),
188
                'response_type' => 'code',
189
            ],
190
            '&'
191
        );
192
193
        return sprintf(
194
            '%s%s%s',
195
            $this->provider->getAuthorizationEndpoint(),
196
            false === strpos($this->provider->getAuthorizationEndpoint(), '?') ? '?' : '&',
197
            $queryParams
198
        );
199
    }
200
201
    /**
202
     * Obtain the access token from the OAuth provider after returning from the
203
     * OAuth provider on the redirectUri (callback URL).
204
     *
205
     * @param string $requestUri    the original authorization
206
     *                              request URL as obtained by getAuthorzationRequestUri
207
     * @param string $responseCode  the code passed to the 'code'
208
     *                              query parameter on the callback URL
209
     * @param string $responseState the state passed to the 'state'
210
     *                              query parameter on the callback URL
211
     *
212
     * @return AccessToken
213
     */
214
    public function handleCallback($requestUri, $responseCode, $responseState)
215
    {
216
        if (is_null($this->userId)) {
217
            throw new OAuthException('userId not set');
218
        }
219
220
        // the requestUri parameter is provided by the caller of this call, and
221
        // does NOT contain external input so does not need to be validated
222
        $requestParameters = self::parseRequestUri($requestUri);
223
        if ($responseState !== $requestParameters['state']) {
224
            // the OAuth state from the initial request MUST be the same as the
225
            // state used by the response
226
            throw new OAuthException('invalid OAuth state');
227
        }
228
229
        if ($requestParameters['client_id'] !== $this->provider->getId()) {
230
            // the client_id used for the initial request differs from the
231
            // currently configured Provider, the client_id MUST be identical
232
            throw new OAuthException('unexpected client identifier');
233
        }
234
235
        // prepare access_token request
236
        $tokenRequestData = [
237
            'client_id' => $this->provider->getId(),
238
            'grant_type' => 'authorization_code',
239
            'code' => $responseCode,
240
            'redirect_uri' => $requestParameters['redirect_uri'],
241
        ];
242
243
        $responseData = $this->validateTokenResponse(
244
            $this->httpClient->post(
245
                $this->provider->getTokenEndpoint(),
246
                $tokenRequestData,
247
                [
248
                    'Authorization' => sprintf(
249
                        'Basic %s',
250
                        Base64::encode(
251
                            sprintf('%s:%s', $this->provider->getId(), $this->provider->getSecret())
252
                        )
253
                    ),
254
                ]
255
            ),
256
            $requestParameters['scope']
257
        );
258
259
        $this->tokenStorage->setAccessToken(
260
            $this->userId,
261
            new AccessToken(
262
                $responseData['access_token'],
263
                $responseData['token_type'],
264
                $responseData['scope'],
265
                array_key_exists('refresh_token', $responseData) ? $responseData['refresh_token'] : null,
266
                $responseData['expires_at']
267
            )
268
        );
269
    }
270
271
    /**
272
     * Refresh the access token from the OAuth.
273
     *
274
     * @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...
275
     * @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...
276
     *                             obtained access token
277
     *
278
     * @return AccessToken
279
     */
280
    private function refreshAccessToken(AccessToken $accessToken)
281
    {
282
        // prepare access_token request
283
        $tokenRequestData = [
284
            'grant_type' => 'refresh_token',
285
            'refresh_token' => $accessToken->getRefreshToken(),
286
            'scope' => $accessToken->getScope(),
287
        ];
288
289
        $responseData = $this->validateTokenResponse(
290
            $this->httpClient->post(
291
                $this->provider->getTokenEndpoint(),
292
                $tokenRequestData,
293
                [
294
                    'Authorization' => sprintf(
295
                        'Basic %s',
296
                        Base64::encode(
297
                            sprintf('%s:%s', $this->provider->getId(), $this->provider->getSecret())
298
                        )
299
                    ),
300
                ]
301
            ),
302
            $accessToken->getScope()
303
        );
304
305
        return new AccessToken(
306
            $responseData['access_token'],
307
            $responseData['token_type'],
308
            $responseData['scope'],
309
            // if a new refresh_token was provided use that, if not reuse the old one
310
            array_key_exists('refresh_token', $responseData) ? $responseData['refresh_token'] : $accessToken->getRefreshToken(),
311
            $responseData['expires_at']
312
        );
313
    }
314
315
    /**
316
     * Validate the provided URI to see if it has the right format, it is
317
     * provided by the API consumer.
318
     */
319
    private static function parseRequestUri($requestUri)
320
    {
321
        if (!is_string($requestUri)) {
322
            throw new InvalidArgumentException('"requestUri" MUST be string');
323
        }
324
325
        if (false === strpos($requestUri, '?')) {
326
            throw new OAuthException('"requestUri" not valid, no query string');
327
        }
328
329
        parse_str(explode('?', $requestUri)[1], $requestParameters);
330
331
        $requiredParameters = [
332
            'client_id',
333
            'redirect_uri',
334
            'scope',
335
            'state',
336
            'response_type',
337
        ];
338
339
        // all of the above parameters were part of the requestUri, make sure
340
        // they are still there...
341 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...
342
            if (!array_key_exists($requiredParameter, $requestParameters)) {
343
                throw new OAuthException(
344
                    sprintf(
345
                        'request URI not valid, missing required query parameter "%s"',
346
                        $requiredParameter
347
                    )
348
                );
349
            }
350
        }
351
352
        return $requestParameters;
353
    }
354
355
    private function validateTokenResponse(Response $response, $requestScope)
356
    {
357
        $tokenResponse = $response->json();
358
        // XXX what if not array?
359
360
        // check if an error occurred
361
        if (array_key_exists('error', $tokenResponse)) {
362
            if (array_key_exists('error_description', $tokenResponse)) {
363
                throw new OAuthServerException(sprintf('%s: %s', $tokenResponse['error'], $tokenResponse['error_description']));
364
            }
365
366
            throw new OAuthServerException($tokenResponse['error']);
367
        }
368
369
        $requiredParameters = [
370
            'access_token',
371
            'token_type',
372
        ];
373
374 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...
375
            if (!array_key_exists($requiredParameter, $tokenResponse)) {
376
                throw new OAuthException(
377
                    sprintf(
378
                        'token response not valid, missing required parameter "%s"',
379
                        $requiredParameter
380
                    )
381
                );
382
            }
383
        }
384
385
        if (!array_key_exists('scope', $tokenResponse)) {
386
            // if the token endpoint does not return a 'scope' value, the
387
            // specification says the requested scope was granted
388
            $tokenResponse['scope'] = $requestScope;
389
        }
390
391
        $tokenResponse['expires_at'] = $this->calculateExpiresAt($tokenResponse);
392
393
        return $tokenResponse;
394
    }
395
396
    private function calculateExpiresAt(array $tokenResponse)
397
    {
398
        $dateTime = clone $this->dateTime;
399
        if (array_key_exists('expires_in', $tokenResponse)) {
400
            return date_add($dateTime, new DateInterval(sprintf('PT%dS', $tokenResponse['expires_in'])));
401
        }
402
403
        // if the 'expires_in' field is not available, we default to 1 year
404
        return date_add($dateTime, new DateInterval('P1Y'));
405
    }
406
}
407