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 ( d56106...8b0d2c )
by François
12:57
created

OAuthClient   B

Complexity

Total Complexity 44

Size/Duplication

Total Lines 463
Duplicated Lines 6.05 %

Coupling/Cohesion

Components 1
Dependencies 14

Importance

Changes 0
Metric Value
wmc 44
lcom 1
cbo 14
dl 28
loc 463
rs 8.3396
c 0
b 0
f 0

16 Methods

Rating   Name   Duplication   Size   Complexity  
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
A get() 0 4 1
A post() 0 4 1
C send() 0 41 7
B getAuthorizeUri() 0 38 3
C handleCallback() 14 85 10
A hasAccessToken() 0 21 4
B refreshAccessToken() 14 63 6
A getAccessToken() 0 16 4
A hashCodeVerifier() 0 13 1
A generateCodeVerifier() 0 9 1

How to fix   Duplicated Code    Complexity   

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:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like OAuthClient often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use OAuthClient, and based on these observations, apply Extract Interface, too.

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
     */
63
    public function __construct(TokenStorageInterface $tokenStorage, HttpClientInterface $httpClient)
64
    {
65
        $this->tokenStorage = $tokenStorage;
66
        $this->httpClient = $httpClient;
67
68
        $this->session = new Session();
69
        $this->random = new Random();
70
        $this->dateTime = new DateTime();
71
    }
72
73
    /**
74
     * @param Provider $provider
75
     */
76
    public function setProvider(Provider $provider)
77
    {
78
        $this->provider = $provider;
79
    }
80
81
    /**
82
     * @param SessionInterface $session
83
     */
84
    public function setSession(SessionInterface $session)
85
    {
86
        $this->session = $session;
87
    }
88
89
    /**
90
     * @param RandomInterface $random
91
     */
92
    public function setRandom(RandomInterface $random)
93
    {
94
        $this->random = $random;
95
    }
96
97
    /**
98
     * @param DateTime $dateTime
99
     */
100
    public function setDateTime(DateTime $dateTime)
101
    {
102
        $this->dateTime = $dateTime;
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
        $sessionData = $this->session->get('_oauth2_session');
257
258
        // delete the session, we don't want it to be used multiple times...
259
        $this->session->del('_oauth2_session');
260
261
        if (!hash_equals($sessionData['state'], $responseState)) {
262
            // the OAuth state from the initial request MUST be the same as the
263
            // state used by the response
264
            throw new OAuthException('invalid session (state)');
265
        }
266
267
        // session providerId MUST match current set Provider
268
        if ($sessionData['provider_id'] !== $this->provider->getProviderId()) {
269
            throw new OAuthException('invalid session (provider_id)');
270
        }
271
272
        // session userId MUST match current set userId
273
        if ($sessionData['user_id'] !== $this->userId) {
274
            throw new OAuthException('invalid session (user_id)');
275
        }
276
277
        // prepare access_token request
278
        $tokenRequestData = [
279
            'client_id' => $this->provider->getClientId(),
280
            'grant_type' => 'authorization_code',
281
            'code' => $responseCode,
282
            'redirect_uri' => $sessionData['redirect_uri'],
283
            'code_verifier' => $sessionData['code_verifier'],
284
        ];
285
286
        $requestHeaders = [];
287
        // if we have a secret registered for the client, use it
288 View Code Duplication
        if (!is_null($this->provider->getSecret())) {
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...
289
            $requestHeaders = [
290
                'Authorization' => sprintf(
291
                    'Basic %s',
292
                    Base64::encode(
293
                        sprintf('%s:%s', $this->provider->getClientId(), $this->provider->getSecret())
294
                    )
295
                ),
296
            ];
297
        }
298
299
        $response = $this->httpClient->send(
300
            Request::post(
301
                $this->provider->getTokenEndpoint(),
302
                $tokenRequestData,
303
                $requestHeaders
304
            )
305
        );
306
307
        if (400 === $response->getStatusCode()) {
308
            // check for "invalid_grant"
309
            $responseData = $response->json();
310 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...
311
                // not an "invalid_grant", we can't deal with this here...
312
                throw new OAuthServerException($response);
313
            }
314
315
            throw new OAuthException('authorization_code was not accepted by the server');
316
        }
317
318
        if (!$response->isOkay()) {
319
            // if there is any other error, we can't deal with this here...
320
            throw new OAuthServerException($response);
321
        }
322
323
        $this->tokenStorage->storeAccessToken(
324
            $this->userId,
325
            AccessToken::fromCodeResponse(
326
                $this->provider,
327
                $this->dateTime,
328
                $response->json(),
329
                // in case server does not return a scope, we know it granted
330
                // our requested scope
331
                $sessionData['scope']
332
            )
333
        );
334
    }
335
336
    /**
337
     * Verify if an AccessToken in the list that matches this scope, bound to
338
     * providerId and userId.
339
     *
340
     * This method has NO side effects, i.e. it will not try to use, refresh or
341
     * delete AccessTokens. If a token is expired, but a refresh token is
342
     * available it is assumed that an AccessToken is available.
343
     *
344
     * NOTE: this does not mean that the token will also be accepted by the
345
     * resource server!
346
     *
347
     * @param string $scope
348
     *
349
     * @return bool
350
     */
351
    public function hasAccessToken($scope)
352
    {
353
        if (false === $accessToken = $this->getAccessToken($scope)) {
354
            return false;
355
        }
356
357
        // is it expired? but do we have a refresh_token?
358
        if ($accessToken->isExpired($this->dateTime)) {
359
            // access_token is expired
360
            if (!is_null($accessToken->getRefreshToken())) {
361
                // but we have a refresh_token
362
                return true;
363
            }
364
365
            // no refresh_token
366
            return false;
367
        }
368
369
        // not expired
370
        return true;
371
    }
372
373
    /**
374
     * @param AccessToken $accessToken
375
     *
376
     * @return AccessToken|false
377
     */
378
    private function refreshAccessToken(AccessToken $accessToken)
379
    {
380
        // prepare access_token request
381
        $tokenRequestData = [
382
            'grant_type' => 'refresh_token',
383
            'refresh_token' => $accessToken->getRefreshToken(),
384
            'scope' => $accessToken->getScope(),
385
        ];
386
387
        $requestHeaders = [];
388
        // if we have a secret registered for the client, use it
389 View Code Duplication
        if (!is_null($this->provider->getSecret())) {
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...
390
            $requestHeaders = [
391
                'Authorization' => sprintf(
392
                    'Basic %s',
393
                    Base64::encode(
394
                        sprintf('%s:%s', $this->provider->getClientId(), $this->provider->getSecret())
395
                    )
396
                ),
397
            ];
398
        }
399
400
        $response = $this->httpClient->send(
401
            Request::post(
402
                $this->provider->getTokenEndpoint(),
403
                $tokenRequestData,
404
                $requestHeaders
405
            )
406
        );
407
408
        if (400 === $response->getStatusCode()) {
409
            // check for "invalid_grant"
410
            $responseData = $response->json();
411 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...
412
                // not an "invalid_grant", we can't deal with this here...
413
                throw new OAuthServerException($response);
414
            }
415
416
            // delete the access_token, we assume the user revoked it
417
            $this->tokenStorage->deleteAccessToken($this->userId, $accessToken);
418
419
            return false;
420
        }
421
422
        if (!$response->isOkay()) {
423
            // if there is any other error, we can't deal with this here...
424
            throw new OAuthServerException($response);
425
        }
426
427
        $accessToken = AccessToken::fromRefreshResponse(
428
            $this->provider,
429
            $this->dateTime,
430
            $response->json(),
431
            // provide the old AccessToken to borrow some fields if the server
432
            // does not provide them on "refresh"
433
            $accessToken
434
        );
435
436
        // store the refreshed AccessToken
437
        $this->tokenStorage->storeAccessToken($this->userId, $accessToken);
438
439
        return $accessToken;
440
    }
441
442
    /**
443
     * Find an AccessToken in the list that matches this scope, bound to
444
     * providerId and userId.
445
     *
446
     * @param string $scope
447
     *
448
     * @return AccessToken|false
449
     */
450
    private function getAccessToken($scope)
451
    {
452
        $accessTokenList = $this->tokenStorage->getAccessTokenList($this->userId);
453
        foreach ($accessTokenList as $accessToken) {
454
            if ($this->provider->getProviderId() !== $accessToken->getProviderId()) {
455
                continue;
456
            }
457
            if ($scope !== $accessToken->getScope()) {
458
                continue;
459
            }
460
461
            return $accessToken;
462
        }
463
464
        return false;
465
    }
466
467
    /**
468
     * @param string $codeVerifier
469
     *
470
     * @return string
471
     */
472
    private static function hashCodeVerifier($codeVerifier)
473
    {
474
        return rtrim(
475
            Base64UrlSafe::encode(
476
                hash(
477
                    'sha256',
478
                    $codeVerifier,
479
                    true
480
                )
481
            ),
482
            '='
483
        );
484
    }
485
486
    /**
487
     * @return string
488
     */
489
    private function generateCodeVerifier()
490
    {
491
        return rtrim(
492
            Base64UrlSafe::encode(
493
                $this->random->get(32, true)
494
            ),
495
            '='
496
        );
497
    }
498
}
499