Completed
Pull Request — master (#73)
by Tobias
02:40
created

Authenticator::getCode()   B

Complexity

Conditions 6
Paths 6

Size

Total Lines 34
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

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