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 ( 5442c0...516271 )
by François
02:38
created

OAuthClient::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 10
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

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