Completed
Pull Request — master (#73)
by Tobias
04:16 queued 10s
created

Authenticator::fetchNewAccessToken()   B

Complexity

Conditions 3
Paths 3

Size

Total Lines 26
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
c 2
b 0
f 0
dl 0
loc 26
rs 8.8571
cc 3
eloc 13
nc 3
nop 1
1
<?php
2
3
namespace Happyr\LinkedIn;
4
5
use Happyr\LinkedIn\Exception\LinkedInTransferException;
6
use Happyr\LinkedIn\Exception\LinkedInException;
7
use Happyr\LinkedIn\Http\GlobalVariableGetter;
8
use Happyr\LinkedIn\Http\RequestManager;
9
use Happyr\LinkedIn\Http\ResponseConverter;
10
use Happyr\LinkedIn\Http\UrlGeneratorInterface;
11
use Happyr\LinkedIn\Storage\DataStorageInterface;
12
use Happyr\LinkedIn\Storage\SessionStorage;
13
14
class Authenticator
15
{
16
    /**
17
     * The Application ID.
18
     *
19
     * @var string
20
     */
21
    protected $appId;
22
23
    /**
24
     * The Application App Secret.
25
     *
26
     * @var string
27
     */
28
    protected $appSecret;
29
30
    /**
31
     * A CSRF state variable to assist in the defense against CSRF attacks.
32
     */
33
    protected $state;
34
35
    /**
36
     * @var DataStorageInterface storage
37
     */
38
    private $storage;
39
40
    /**
41
     * @var RequestManager
42
     */
43
    private $requestManager;
44
45
    /**
46
     * @param RequestManager $requestManager
47
     * @param string         $appId
48
     * @param string         $appSecret
49
     */
50
    public function __construct(RequestManager $requestManager, $appId, $appSecret)
51
    {
52
        $this->appId = $appId;
53
        $this->appSecret = $appSecret;
54
        $this->requestManager = $requestManager;
55
    }
56
57
    /**
58
     * Determines and returns the user access token using the authorization code. The intent is to
59
     * return a valid access token, or null if one is determined to not be available.
60
     *
61
     * @param UrlGeneratorInterface $urlGenerator
62
     *
63
     * @return AccessToken|null A valid user access token, or null if one could not be determined.
64
     *
65
     * @throws LinkedInTransferException
66
     */
67
    public function fetchNewAccessToken(UrlGeneratorInterface $urlGenerator)
68
    {
69
        $storage = $this->getStorage();
70
        $code = $this->getCode();
71
72
        if ($code === null) {
73
            // as a fallback, just return whatever is in the persistent
74
            // store, knowing nothing explicit (signed request, authorization
75
            // code, etc.) was present to shadow it (or we saw a code in $_REQUEST,
76
            // but it's the same as what's in the persistent store)
77
            return $storage->get('access_token', null);
78
        }
79
80
        try {
81
            $accessToken = $this->getAccessTokenFromCode($urlGenerator, $code);
82
        } catch (LinkedInException $e) {
83
            // code was bogus, so everything based on it should be invalidated.
84
            $storage->clearAll();
85
            throw $e;
86
        }
87
88
        $storage->set('code', $code);
89
        $storage->set('access_token', $accessToken);
90
91
        return $accessToken;
92
    }
93
94
    /**
95
     * Retrieves an access token for the given authorization code
96
     * (previously generated from www.linkedin.com on behalf of
97
     * a specific user). The authorization code is sent to www.linkedin.com
98
     * and a legitimate access token is generated provided the access token
99
     * and the user for which it was generated all match, and the user is
100
     * either logged in to LinkedIn or has granted an offline access permission.
101
     *
102
     * @param UrlGeneratorInterface $urlGenerator
103
     * @param string                $code         An authorization code.
104
     *
105
     * @return AccessToken An access token exchanged for the authorization code.
106
     */
107
    protected function getAccessTokenFromCode(UrlGeneratorInterface $urlGenerator, $code)
108
    {
109
        if (empty($code)) {
110
            throw new LinkedInException('Could not get access token: The code was empty.');
111
        }
112
113
        $redirectUri = $this->getStorage()->get('redirect_url');
114
        try {
115
            $url = $urlGenerator->getUrl('www', 'uas/oauth2/accessToken');
116
            $headers = ['Content-Type' => 'application/x-www-form-urlencoded'];
117
            $body = http_build_query(
118
                [
119
                    'grant_type' => 'authorization_code',
120
                    'code' => $code,
121
                    'redirect_uri' => $redirectUri,
122
                    'client_id' => $this->appId,
123
                    'client_secret' => $this->appSecret,
124
                ]
125
            );
126
127
            $response = ResponseConverter::convertToArray($this->getRequestManager()->sendRequest('POST', $url, $headers, $body));
0 ignored issues
show
Documentation introduced by
$body is of type string, but the function expects a null.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
128
        } catch (LinkedInTransferException $e) {
129
            // most likely that user very recently revoked authorization.
130
            // In any event, we don't have an access token, so throw an exception.
131
            throw new LinkedInException('Could not get access token: The user may have revoked the authorization response from LinkedIn.com was empty.', 0, $e);
132
        }
133
134
        if (empty($response)) {
135
            throw new LinkedInException('Could not get access token: The response from LinkedIn.com was empty.');
136
        }
137
138
        $tokenData = array_merge(array('access_token' => null, 'expires_in' => null), $response);
139
        $token = new AccessToken($tokenData['access_token'], $tokenData['expires_in']);
140
141
        if (!$token->hasToken()) {
142
            throw new LinkedInException('Could not get access token: The response from LinkedIn.com did not contain a token.');
143
        }
144
145
        return $token;
146
    }
147
148
    /**
149
     * Get a Login URL for use with redirects. By default, full page redirect is
150
     * assumed. If you are using the generated URL with a window.open() call in
151
     * JavaScript, you can pass in display=popup as part of the $params.
152
     *
153
     * The parameters:
154
     * - redirect_uri: the url to go to after a successful login
155
     * - scope: comma (or space) separated list of requested extended permissions
156
     *
157
     * @param UrlGeneratorInterface $urlGenerator
158
     * @param array                 $options      Provide custom parameters
159
     *
160
     * @return string The URL for the login flow
161
     */
162
    public function getLoginUrl(UrlGeneratorInterface $urlGenerator, $options = array())
163
    {
164
        // Generate a state
165
        $this->establishCSRFTokenState();
166
167
        // Build request params
168
        $requestParams = array(
169
            'response_type' => 'code',
170
            'client_id' => $this->appId,
171
            'state' => $this->getState(),
172
        );
173
174
        // Look for the redirect URL
175
        if (isset($options['redirect_uri'])) {
176
            $requestParams['redirect_uri'] = $options['redirect_uri'];
177
        } else {
178
            $requestParams['redirect_uri'] = $urlGenerator->getCurrentUrl();
179
        }
180
181
        // Save the redirect url for later
182
        $this->getStorage()->set('redirect_url', $requestParams['redirect_uri']);
183
184
        // if 'scope' is passed as an array, convert to space separated list
185
        $scopeParams = isset($options['scope']) ? $options['scope'] : null;
186
        if ($scopeParams) {
187
            //if scope is an array
188
            if (is_array($scopeParams)) {
189
                $requestParams['scope'] = implode(' ', $scopeParams);
190
            } elseif (is_string($scopeParams)) {
191
                //if scope is a string with ',' => make it to an array
192
                $requestParams['scope'] = str_replace(',', ' ', $scopeParams);
193
            }
194
        }
195
196
        return $urlGenerator->getUrl('www', 'uas/oauth2/authorization', $requestParams);
197
    }
198
199
    /**
200
     * Get the authorization code from the query parameters, if it exists,
201
     * and otherwise return null to signal no authorization code was
202
     * discoverable.
203
     *
204
     * @return string|null The authorization code, or null if the authorization code could not be determined.
205
     *
206
     * @throws LinkedInTransferException
207
     */
208
    protected function getCode()
209
    {
210
        $storage = $this->getStorage();
211
212
        if (!GlobalVariableGetter::has('code')) {
213
            return;
214
        }
215
216
        if ($storage->get('code') === GlobalVariableGetter::get('code')) {
217
            //we have already validated this code
218
            return;
219
        }
220
221
        //if stored state does not exists
222
        if (null === $state = $this->getState()) {
223
            throw new LinkedInException('Could not find a stored CSRF state token.');
224
        }
225
226
        //if state exists in the request
227
        if (!GlobalVariableGetter::has('state')) {
228
            throw new LinkedInException('Could not find a CSRF state token in the request.');
229
        }
230
231
        //if state exists in session and in request and if they are not equal
232
        if ($state !== GlobalVariableGetter::get('state')) {
233
            throw new LinkedInException('The CSRF state token from the request does not match the stored token.');
234
        }
235
236
        // CSRF state has done its job, so clear it
237
        $this->setState(null);
238
        $storage->clear('state');
239
240
        return GlobalVariableGetter::get('code');
241
    }
242
243
    /**
244
     * Lays down a CSRF state token for this process.
245
     */
246
    protected function establishCSRFTokenState()
247
    {
248
        if ($this->getState() === null) {
249
            $this->setState(md5(uniqid(mt_rand(), true)));
250
            $this->getStorage()->set('state', $this->getState());
251
        }
252
    }
253
254
    /**
255
     * Clear the storage.
256
     *
257
     * @return $this
258
     */
259
    public function clearStorage()
260
    {
261
        $this->getStorage()->clearAll();
262
263
        return $this;
264
    }
265
266
    /**
267
     * Get the state, use this to verify the CSRF token.
268
     *
269
     *
270
     * @return string|null
271
     */
272
    protected function getState()
273
    {
274
        if ($this->state === null) {
275
            $this->state = $this->getStorage()->get('state', null);
276
        }
277
278
        return $this->state;
279
    }
280
281
    /**
282
     * @param $state
283
     *
284
     * @return $this
285
     */
286
    protected function setState($state)
287
    {
288
        $this->state = $state;
289
290
        return $this;
291
    }
292
293
    /**
294
     * @return DataStorageInterface
295
     */
296
    protected function getStorage()
297
    {
298
        if ($this->storage === null) {
299
            $this->storage = new SessionStorage();
300
        }
301
302
        return $this->storage;
303
    }
304
305
    /**
306
     * @param DataStorageInterface $storage
307
     *
308
     * @return $this
309
     */
310
    public function setStorage(DataStorageInterface $storage)
311
    {
312
        $this->storage = $storage;
313
314
        return $this;
315
    }
316
317
    /**
318
     * @return RequestManager
319
     */
320
    protected function getRequestManager()
321
    {
322
        return $this->requestManager;
323
    }
324
}
325