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 ( 4207dd...1fea29 )
by François
35:40
created

OAuth2Client::getAccessToken()   B

Complexity

Conditions 4
Paths 3

Size

Total Lines 41
Code Lines 22

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 41
rs 8.5806
cc 4
eloc 22
nc 3
nop 3
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 InvalidArgumentException;
26
27
/**
28
 * OAuth 2.0 Client. Helper class to make it easy to obtain an access token
29
 * from an OAuth 2.0 provider.
30
 */
31
class OAuth2Client
32
{
33
    /** @var Provider */
34
    private $provider;
35
36
    /** @var HttpClientInterface */
37
    private $httpClient;
38
39
    /** @var RandomInterface */
40
    private $random;
41
42
    /** @var \DateTime */
43
    private $dateTime;
44
45
    /**
46
     * Instantiate an OAuth 2.0 Client.
47
     *
48
     * @param Provider            $provider   the OAuth 2.0 provider configuration
49
     * @param HttpClientInterface $httpClient the HTTP client implementation
50
     * @param RandomInterface     $random     the random implementation
51
     */
52
    public function __construct(Provider $provider, HttpClientInterface $httpClient, RandomInterface $random = null, DateTime $dateTime = null)
53
    {
54
        $this->provider = $provider;
55
        $this->httpClient = $httpClient;
56
        if (is_null($random)) {
57
            $random = new Random();
58
        }
59
        $this->random = $random;
60
        if (is_null($dateTime)) {
61
            $dateTime = new DateTime();
62
        }
63
        $this->dateTime = $dateTime;
64
    }
65
66
    /**
67
     * Obtain an authorization request URL to start the authorization process
68
     * at the OAuth provider.
69
     *
70
     * @param string $scope       the space separated scope tokens
71
     * @param string $redirectUri the URL to redirect back to after coming back
72
     *                            from the OAuth provider (callback URL)
73
     *
74
     * @return string the authorization request URL
75
     *
76
     * @see https://tools.ietf.org/html/rfc6749#section-3.3
77
     * @see https://tools.ietf.org/html/rfc6749#section-3.1.2
78
     */
79
    public function getAuthorizationRequestUri($scope, $redirectUri)
80
    {
81
        $queryParams = http_build_query(
82
            [
83
                'client_id' => $this->provider->getId(),
84
                'redirect_uri' => $redirectUri,
85
                'scope' => $scope,
86
                'state' => $this->random->get(16),
87
                'response_type' => 'code',
88
            ],
89
            '&'
90
        );
91
92
        return sprintf(
93
            '%s%s%s',
94
            $this->provider->getAuthorizationEndpoint(),
95
            false === strpos($this->provider->getAuthorizationEndpoint(), '?') ? '?' : '&',
96
            $queryParams
97
        );
98
    }
99
100
    /**
101
     * Obtain the access token from the OAuth provider after returning from the
102
     * OAuth provider on the redirectUri (callback URL).
103
     *
104
     * @param string $requestUri    the original authorization
105
     *                              request URL as obtained by getAuthorzationRequestUri
106
     * @param string $responseCode  the code passed to the 'code'
107
     *                              query parameter on the callback URL
108
     * @param string $responseState the state passed to the 'state'
109
     *                              query parameter on the callback URL
110
     *
111
     * @return AccessToken
112
     */
113
    public function getAccessToken($requestUri, $responseCode, $responseState)
114
    {
115
        // the requestUri parameter is provided by the caller of this call, and
116
        // does NOT contain external input so does not need to be validated
117
        $requestParameters = self::parseRequestUri($requestUri);
118
        if ($responseState !== $requestParameters['state']) {
119
            // the OAuth state from the initial request MUST be the same as the
120
            // state used by the response
121
            throw new OAuthException('invalid OAuth state');
122
        }
123
124
        if ($requestParameters['client_id'] !== $this->provider->getId()) {
125
            // the client_id used for the initial request differs from the
126
            // currently configured Provider, the client_id MUST be identical
127
            throw new OAuthException('unexpected client identifier');
128
        }
129
130
        // prepare access_token request
131
        $tokenRequestData = [
132
            'client_id' => $this->provider->getId(),
133
            'grant_type' => 'authorization_code',
134
            'code' => $responseCode,
135
            'redirect_uri' => $requestParameters['redirect_uri'],
136
        ];
137
138
        $responseData = $this->validateTokenResponse(
139
            $this->httpClient->post(
140
                $this->provider,
141
                $tokenRequestData
142
            ),
143
            $requestParameters['scope']
144
        );
145
146
        return new AccessToken(
147
            $responseData['access_token'],
148
            $responseData['token_type'],
149
            $responseData['scope'],
150
            array_key_exists('refresh_token', $responseData) ? $responseData['refresh_token'] : null,
151
            $responseData['expires_at']
152
        );
153
    }
154
155
    /**
156
     * Refresh the access token from the OAuth.
157
     *
158
     * @param string $refreshToken the refresh token
159
     * @param string $requestScope the scope associated with the previously
160
     *                             obtained access token
161
     *
162
     * @return AccessToken
163
     */
164
    public function refreshAccessToken($refreshToken, $requestScope)
165
    {
166
        // prepare access_token request
167
        $tokenRequestData = [
168
            'grant_type' => 'refresh_token',
169
            'refresh_token' => $refreshToken,
170
            'scope' => $requestScope,
171
        ];
172
173
        $responseData = $this->validateTokenResponse(
174
            $this->httpClient->post(
175
                $this->provider,
176
                $tokenRequestData
177
            ),
178
            $requestScope
179
        );
180
181
        return new AccessToken(
182
            $responseData['access_token'],
183
            $responseData['token_type'],
184
            $responseData['scope'],
185
            array_key_exists('refresh_token', $responseData) ? $responseData['refresh_token'] : null,
186
            $responseData['expires_at']
187
        );
188
    }
189
190
    /**
191
     * Validate the provided URI to see if it has the right format, it is
192
     * provided by the API consumer.
193
     */
194
    private static function parseRequestUri($requestUri)
195
    {
196
        if (!is_string($requestUri)) {
197
            throw new InvalidArgumentException('"requestUri" MUST be string');
198
        }
199
200
        if (false === strpos($requestUri, '?')) {
201
            throw new OAuthException('"requestUri" not valid, no query string');
202
        }
203
204
        parse_str(explode('?', $requestUri)[1], $requestParameters);
205
206
        $requiredParameters = [
207
            'client_id',
208
            'redirect_uri',
209
            'scope',
210
            'state',
211
            'response_type',
212
        ];
213
214
        // all of the above parameters were part of the requestUri, make sure
215
        // they are still there...
216 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...
217
            if (!array_key_exists($requiredParameter, $requestParameters)) {
218
                throw new OAuthException(
219
                    sprintf(
220
                        'request URI not valid, missing required query parameter "%s"',
221
                        $requiredParameter
222
                    )
223
                );
224
            }
225
        }
226
227
        return $requestParameters;
228
    }
229
230
    private function validateTokenResponse(array $tokenResponse, $requestScope)
231
    {
232
        // check if an error occurred
233
        if (array_key_exists('error', $tokenResponse)) {
234
            if (array_key_exists('error_description', $tokenResponse)) {
235
                throw new OAuthServerException(sprintf('%s: %s', $tokenResponse['error'], $tokenResponse['error_description']));
236
            }
237
238
            throw new OAuthServerException($tokenResponse['error']);
239
        }
240
241
        $requiredParameters = [
242
            'access_token',
243
            'token_type',
244
        ];
245
246 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...
247
            if (!array_key_exists($requiredParameter, $tokenResponse)) {
248
                throw new OAuthException(
249
                    sprintf(
250
                        'token response not valid, missing required parameter "%s"',
251
                        $requiredParameter
252
                    )
253
                );
254
            }
255
        }
256
257
        if (!array_key_exists('scope', $tokenResponse)) {
258
            // if the token endpoint does not return a 'scope' value, the
259
            // specification says the requested scope was granted
260
            $tokenResponse['scope'] = $requestScope;
261
        }
262
263
        $tokenResponse['expires_at'] = $this->calculateExpiresAt($tokenResponse);
264
265
        return $tokenResponse;
266
    }
267
268
    private function calculateExpiresAt(array $tokenResponse)
269
    {
270
        $dateTime = clone $this->dateTime;
271
        if (array_key_exists('expires_in', $tokenResponse)) {
272
            return date_add($dateTime, new DateInterval(sprintf('PT%dS', $tokenResponse['expires_in'])));
273
        }
274
275
        // if the 'expires_in' field is not available, we default to 1 year
276
        return date_add($dateTime, new DateInterval('P1Y'));
277
    }
278
}
279