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 ( d523a5...3ad8b5 )
by François
02:47
created

OAuthClient::handleCallback()   C

Complexity

Conditions 9
Paths 8

Size

Total Lines 78
Code Lines 39

Duplication

Lines 4
Ratio 5.13 %

Importance

Changes 0
Metric Value
dl 4
loc 78
rs 5.7191
c 0
b 0
f 0
cc 9
eloc 39
nc 8
nop 2

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
/**
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
35
class OAuthClient
36
{
37
    /** @var TokenStorageInterface */
38
    private $tokenStorage;
39
40
    /** @var \fkooman\OAuth\Client\Http\HttpClientInterface */
41
    private $httpClient;
42
43
    /** @var SessionInterface */
44
    private $session;
45
46
    /** @var RandomInterface */
47
    private $random;
48
49
    /** @var \DateTime */
50
    private $dateTime;
51
52
    /** @var array */
53
    private $providerList = [];
54
55
    /** @var string */
56
    private $providerId = null;
57
58
    /** @var string */
59
    private $userId = null;
60
61
    /**
62
     * @param TokenStorageInterface    $tokenStorage
63
     * @param Http\HttpClientInterface $httpClient
64
     */
65
    public function __construct(TokenStorageInterface $tokenStorage, HttpClientInterface $httpClient)
66
    {
67
        $this->tokenStorage = $tokenStorage;
68
        $this->httpClient = $httpClient;
69
70
        $this->session = new Session();
71
        $this->random = new Random();
72
        $this->dateTime = new DateTime();
73
    }
74
75
    /**
76
     * @param string   $providerId
77
     * @param Provider $provider
78
     */
79
    public function addProvider($providerId, Provider $provider)
80
    {
81
        $this->providerList[$providerId] = $provider;
82
        // the first provider we add becomes the active provider, can be
83
        // overridden by the "setProviderId" method
84
        if (1 === count($this->providerList)) {
85
            $this->providerId = $providerId;
86
        }
87
    }
88
89
    /**
90
     * @param string $providerId
91
     */
92
    public function setProviderId($providerId)
93
    {
94
        if (!array_key_exists($providerId, $this->providerList)) {
95
            throw new OAuthException(sprintf('provider with providerId "%s" does not exist', $this->providerId));
96
        }
97
    }
98
99
    /**
100
     * @param SessionInterface $session
101
     */
102
    public function setSession(SessionInterface $session)
103
    {
104
        $this->session = $session;
105
    }
106
107
    /**
108
     * @param RandomInterface $random
109
     */
110
    public function setRandom(RandomInterface $random)
111
    {
112
        $this->random = $random;
113
    }
114
115
    /**
116
     * @param DateTime $dateTime
117
     */
118
    public function setDateTime(DateTime $dateTime)
119
    {
120
        $this->dateTime = $dateTime;
121
    }
122
123
    /**
124
     * @param string $userId
125
     */
126
    public function setUserId($userId)
127
    {
128
        $this->userId = $userId;
129
    }
130
131
    /**
132
     * Perform a GET request, convenience wrapper for ::send().
133
     *
134
     * @param string $requestScope
135
     * @param string $requestUri
136
     * @param array  $requestHeaders
137
     *
138
     * @return Http\Response|false
139
     */
140
    public function get($requestScope, $requestUri, array $requestHeaders = [])
141
    {
142
        return $this->send($requestScope, Request::get($requestUri, $requestHeaders));
143
    }
144
145
    /**
146
     * Perform a POST request, convenience wrapper for ::send().
147
     *
148
     * @param string $requestScope
149
     * @param string $requestUri
150
     * @param array  $postBody
151
     * @param array  $requestHeaders
152
     *
153
     * @return Http\Response|false
154
     */
155
    public function post($requestScope, $requestUri, array $postBody, array $requestHeaders = [])
156
    {
157
        return $this->send($requestScope, Request::post($requestUri, $postBody, $requestHeaders));
158
    }
159
160
    /**
161
     * Perform a HTTP request.
162
     *
163
     * @param string       $requestScope
164
     * @param Http\Request $request
165
     *
166
     * @return Response|false
167
     */
168
    public function send($requestScope, Request $request)
169
    {
170
        if (is_null($this->userId)) {
171
            throw new OAuthException('userId not set');
172
        }
173
        if (is_null($this->providerId)) {
174
            throw new OAuthException('providerId not set');
175
        }
176
177
        // make sure we have an access token
178
        if (false === $accessToken = $this->tokenStorage->getAccessToken($this->userId, $this->providerId, $requestScope)) {
179
            return false;
180
        }
181
182
        if ($requestScope !== $accessToken->getScope()) {
183
            throw new OAuthException('access_token does not have the required scope');
184
        }
185
186
        if ($accessToken->isExpired($this->dateTime)) {
187
            // access_token is expired, try to refresh it
188
            if (is_null($accessToken->getRefreshToken())) {
189
                // we do not have a refresh_token, delete this access token, it
190
                // is useless now...
191
                $this->tokenStorage->deleteAccessToken($this->userId, $this->providerId, $accessToken);
192
193
                return false;
194
            }
195
196
            // try to refresh the AccessToken
197
            if (false === $accessToken = $this->refreshAccessToken($accessToken)) {
198
                // didn't work
199
                return false;
200
            }
201
        }
202
203
        // add Authorization header to the request headers
204
        $request->setHeader('Authorization', sprintf('Bearer %s', $accessToken->getToken()));
205
206
        $response = $this->httpClient->send($request);
207
        if (401 === $response->getStatusCode()) {
208
            // the access_token was not accepted, but isn't expired, we assume
209
            // the user revoked it, also no need to try with refresh_token
210
            $this->tokenStorage->deleteAccessToken($this->userId, $this->providerId, $accessToken);
211
212
            return false;
213
        }
214
215
        return $response;
216
    }
217
218
    /**
219
     * Obtain an authorization request URL to start the authorization process
220
     * at the OAuth provider.
221
     *
222
     * @param string $scope       the space separated scope tokens
223
     * @param string $redirectUri the URL registered at the OAuth provider, to
224
     *                            be redirected back to
225
     *
226
     * @return string the authorization request URL
227
     *
228
     * @see https://tools.ietf.org/html/rfc6749#section-3.3
229
     * @see https://tools.ietf.org/html/rfc6749#section-3.1.2
230
     */
231
    public function getAuthorizeUri($scope, $redirectUri)
232
    {
233
        $queryParameters = [
234
            'client_id' => $this->getActiveProvider()->getId(),
235
            'redirect_uri' => $redirectUri,
236
            'scope' => $scope,
237
            'state' => $this->random->get(16),
238
            'response_type' => 'code',
239
        ];
240
241
        $authorizeUri = sprintf(
242
            '%s%s%s',
243
            $this->getActiveProvider()->getAuthorizationEndpoint(),
244
            false === strpos($this->getActiveProvider()->getAuthorizationEndpoint(), '?') ? '?' : '&',
245
            http_build_query($queryParameters, '&')
246
        );
247
        $this->session->set('_oauth2_session', array_merge($queryParameters, ['provider_id' => $this->providerId]));
248
249
        return $authorizeUri;
250
    }
251
252
    /**
253
     * @param string $responseCode  the code passed to the "code"
254
     *                              query parameter on the callback URL
255
     * @param string $responseState the state passed to the "state"
256
     *                              query parameter on the callback URL
257
     */
258
    public function handleCallback($responseCode, $responseState)
259
    {
260
        if (is_null($this->userId)) {
261
            throw new OAuthException('userId not set');
262
        }
263
        if (is_null($this->providerId)) {
264
            throw new OAuthException('providerId not set');
265
        }
266
267
        $sessionData = $this->session->get('_oauth2_session');
268
        $this->setProviderId($sessionData['provider_id']);
269
270
        // delete the session, we don't want it to be used multiple times...
271
        $this->session->del('_oauth2_session');
272
273
        if (!hash_equals($sessionData['state'], $responseState)) {
274
            // the OAuth state from the initial request MUST be the same as the
275
            // state used by the response
276
            throw new OAuthException('invalid OAuth state');
277
        }
278
279
        if ($sessionData['client_id'] !== $this->getActiveProvider()->getId()) {
280
            // the client_id used for the initial request differs from the
281
            // currently configured Provider, the client_id MUST be identical
282
            throw new OAuthException('unexpected client identifier');
283
        }
284
285
        // prepare access_token request
286
        $tokenRequestData = [
287
            'client_id' => $this->getActiveProvider()->getId(),
288
            'grant_type' => 'authorization_code',
289
            'code' => $responseCode,
290
            'redirect_uri' => $sessionData['redirect_uri'],
291
        ];
292
293
        $response = $this->httpClient->send(
294
            Request::post(
295
                $this->getActiveProvider()->getTokenEndpoint(),
296
                $tokenRequestData,
297
                [
298
                    'Authorization' => sprintf(
299
                        'Basic %s',
300
                        Base64::encode(
301
                            sprintf('%s:%s', $this->getActiveProvider()->getId(), $this->getActiveProvider()->getSecret())
302
                        )
303
                    ),
304
                ]
305
            )
306
        );
307
308
        if (400 === $response->getStatusCode()) {
309
            // check for "invalid_grant"
310
            $responseData = $response->json();
311 View Code Duplication
            if (!array_key_exists('error', $responseData) || 'invalid_grant' !== $responseData['error']) {
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...
312
                // not an "invalid_grant", we can't deal with this here...
313
                throw new OAuthServerException($response);
314
            }
315
316
            throw new OAuthException('authorization_code was not accepted by the server');
317
        }
318
319
        if (!$response->isOkay()) {
320
            // if there is any other error, we can't deal with this here...
321
            throw new OAuthServerException($response);
322
        }
323
324
        $this->tokenStorage->setAccessToken(
325
            $this->userId,
326
            $this->providerId,
327
            AccessToken::fromCodeResponse(
328
                $this->dateTime,
329
                $response->json(),
330
                // in case server does not return a scope, we know it granted
331
                // our requested scope
332
                $sessionData['scope']
333
            )
334
        );
335
    }
336
337
    /**
338
     * @param AccessToken $accessToken
339
     *
340
     * @return AccessToken|false
341
     */
342
    private function refreshAccessToken(AccessToken $accessToken)
343
    {
344
        // prepare access_token request
345
        $tokenRequestData = [
346
            'grant_type' => 'refresh_token',
347
            'refresh_token' => $accessToken->getRefreshToken(),
348
            'scope' => $accessToken->getScope(),
349
        ];
350
351
        $response = $this->httpClient->send(
352
            Request::post(
353
                $this->getActiveProvider()->getTokenEndpoint(),
354
                $tokenRequestData,
355
                [
356
                    'Authorization' => sprintf(
357
                        'Basic %s',
358
                        Base64::encode(
359
                            sprintf('%s:%s', $this->getActiveProvider()->getId(), $this->getActiveProvider()->getSecret())
360
                        )
361
                    ),
362
                ]
363
            )
364
        );
365
366
        if (400 === $response->getStatusCode()) {
367
            // check for "invalid_grant"
368
            $responseData = $response->json();
369 View Code Duplication
            if (!array_key_exists('error', $responseData) || 'invalid_grant' !== $responseData['error']) {
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...
370
                // not an "invalid_grant", we can't deal with this here...
371
                throw new OAuthServerException($response);
372
            }
373
374
            // delete the access_token, we assume the user revoked it
375
            $this->tokenStorage->deleteAccessToken($this->userId, $this->providerId, $accessToken);
376
377
            return false;
378
        }
379
380
        if (!$response->isOkay()) {
381
            // if there is any other error, we can't deal with this here...
382
            throw new OAuthServerException($response);
383
        }
384
385
        $accessToken = AccessToken::fromRefreshResponse(
386
            $this->dateTime,
387
            $response->json(),
388
            // provide the old AccessToken to borrow some fields if the server
389
            // does not provide them on "refresh"
390
            $accessToken
391
        );
392
393
        // store the refreshed AccessToken
394
        $this->tokenStorage->setAccessToken($this->userId, $this->providerId, $accessToken);
395
396
        return $accessToken;
397
    }
398
399
    /**
400
     * Get the active OAuth provider.
401
     *
402
     * @return Provider
403
     */
404
    private function getActiveProvider()
405
    {
406
        return $this->providerList[$this->providerId];
407
    }
408
}
409