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 ( 8ce98c...96b546 )
by François
02:04
created

OAuthClient   B

Complexity

Total Complexity 36

Size/Duplication

Total Lines 376
Duplicated Lines 2.13 %

Coupling/Cohesion

Components 1
Dependencies 13

Importance

Changes 0
Metric Value
wmc 36
lcom 1
cbo 13
dl 8
loc 376
rs 8.8
c 0
b 0
f 0

13 Methods

Rating   Name   Duplication   Size   Complexity  
A get() 0 4 1
A post() 0 4 1
A __construct() 0 9 1
A setProvider() 0 4 1
A setSession() 0 4 1
A setRandom() 0 4 1
A setDateTime() 0 4 1
A setUserId() 0 4 1
C send() 0 41 7
B getAuthorizeUri() 0 33 3
C handleCallback() 4 78 9
B refreshAccessToken() 4 57 5
A getAccessToken() 0 16 4

How to fix   Duplicated Code   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

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