Completed
Pull Request — master (#73)
by Tobias
03:11 queued 51s
created

Authenticator::fetchNewAccessToken()   B

Complexity

Conditions 3
Paths 3

Size

Total Lines 27
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 3
Bugs 0 Features 0
Metric Value
c 3
b 0
f 0
dl 0
loc 27
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\LinkedInUrlGeneratorInterface;
9
use Happyr\LinkedIn\Http\RequestManager;
10
use Happyr\LinkedIn\Http\ResponseConverter;
11
use Happyr\LinkedIn\Storage\DataStorageInterface;
12
use Happyr\LinkedIn\Storage\SessionStorage;
13
14
/**
15
 * This class is responseble for the authentication process with LinkedIn.
16
 *
17
 * @author Tobias Nyholm <[email protected]>
18
 */
19
class Authenticator
20
{
21
    /**
22
     * The application ID.
23
     *
24
     * @var string
25
     */
26
    protected $appId;
27
28
    /**
29
     * The application secret.
30
     *
31
     * @var string
32
     */
33
    protected $appSecret;
34
35
    /**
36
     * A storage to use to store data between requests.
37
     *
38
     * @var DataStorageInterface storage
39
     */
40
    private $storage;
41
42
    /**
43
     * @var RequestManager
44
     */
45
    private $requestManager;
46
47
    /**
48
     * @param RequestManager $requestManager
49
     * @param string         $appId
50
     * @param string         $appSecret
51
     */
52
    public function __construct(RequestManager $requestManager, $appId, $appSecret)
53
    {
54
        $this->appId = $appId;
55
        $this->appSecret = $appSecret;
56
        $this->requestManager = $requestManager;
57
    }
58
59
    /**
60
     * Tries to get a new access token from data storage or code. If it fails, it will return null.
61
     *
62
     * @param LinkedInUrlGeneratorInterface $urlGenerator
63
     *
64
     * @return AccessToken|null A valid user access token, or null if one could not be fetched.
65
     *
66
     * @throws LinkedInException
67
     */
68
    public function fetchNewAccessToken(LinkedInUrlGeneratorInterface $urlGenerator)
69
    {
70
        $storage = $this->getStorage();
71
        $code = $this->getCode();
72
73
        if ($code === null) {
74
            /*
75
             * As a fallback, just return whatever is in the persistent
76
             * store, knowing nothing explicit (signed request, authorization
77
             *  code, etc.) was present to shadow it.
78
             */
79
            return $storage->get('access_token');
80
        }
81
82
        try {
83
            $accessToken = $this->getAccessTokenFromCode($urlGenerator, $code);
84
        } catch (LinkedInException $e) {
85
            // code was bogus, so everything based on it should be invalidated.
86
            $storage->clearAll();
87
            throw $e;
88
        }
89
90
        $storage->set('code', $code);
91
        $storage->set('access_token', $accessToken);
92
93
        return $accessToken;
94
    }
95
96
    /**
97
     * Retrieves an access token for the given authorization code
98
     * (previously generated from www.linkedin.com on behalf of
99
     * a specific user). The authorization code is sent to www.linkedin.com
100
     * and a legitimate access token is generated provided the access token
101
     * and the user for which it was generated all match, and the user is
102
     * either logged in to LinkedIn or has granted an offline access permission.
103
     *
104
     * @param LinkedInUrlGeneratorInterface $urlGenerator
105
     * @param string                        $code         An authorization code.
106
     *
107
     * @return AccessToken An access token exchanged for the authorization code.
108
     *
109
     * @throws LinkedInException
110
     */
111
    protected function getAccessTokenFromCode(LinkedInUrlGeneratorInterface $urlGenerator, $code)
112
    {
113
        if (empty($code)) {
114
            throw new LinkedInException('Could not get access token: The code was empty.');
115
        }
116
117
        $redirectUri = $this->getStorage()->get('redirect_uri');
118
        try {
119
            $url = $urlGenerator->getUrl('www', 'uas/oauth2/accessToken');
120
            $headers = ['Content-Type' => 'application/x-www-form-urlencoded'];
121
            $body = http_build_query(
122
                [
123
                    'grant_type' => 'authorization_code',
124
                    'code' => $code,
125
                    'redirect_uri' => $redirectUri,
126
                    'client_id' => $this->appId,
127
                    'client_secret' => $this->appSecret,
128
                ]
129
            );
130
131
            $response = ResponseConverter::convertToArray($this->getRequestManager()->sendRequest('POST', $url, $headers, $body));
132
        } catch (LinkedInTransferException $e) {
133
            // most likely that user very recently revoked authorization.
134
            // In any event, we don't have an access token, so throw an exception.
135
            throw new LinkedInException('Could not get access token: The user may have revoked the authorization response from LinkedIn.com was empty.', $e->getCode(), $e);
136
        }
137
138
        if (empty($response)) {
139
            throw new LinkedInException('Could not get access token: The response from LinkedIn.com was empty.');
140
        }
141
142
        $tokenData = array_merge(array('access_token' => null, 'expires_in' => null), $response);
143
        $token = new AccessToken($tokenData['access_token'], $tokenData['expires_in']);
144
145
        if (!$token->hasToken()) {
146
            throw new LinkedInException('Could not get access token: The response from LinkedIn.com did not contain a token.');
147
        }
148
149
        return $token;
150
    }
151
152
    /**
153
     * Generate a login url.
154
     *
155
     * @param LinkedInUrlGeneratorInterface $urlGenerator
156
     * @param array                         $options
157
     *
158
     * @return string
159
     */
160
    public function getLoginUrl(LinkedInUrlGeneratorInterface $urlGenerator, $options = array())
161
    {
162
        // Generate a state
163
        $this->establishCSRFTokenState();
164
165
        // Build request params
166
        $requestParams = array_merge(array(
167
            'response_type' => 'code',
168
            'client_id' => $this->appId,
169
            'state' => $this->getStorage()->get('state'),
170
            'redirect_uri' => null,
171
        ), $options);
172
173
        // Save the redirect url for later
174
        $this->getStorage()->set('redirect_uri', $requestParams['redirect_uri']);
175
176
        // if 'scope' is passed as an array, convert to space separated list
177
        $scopeParams = isset($options['scope']) ? $options['scope'] : null;
178
        if ($scopeParams) {
179
            //if scope is an array
180
            if (is_array($scopeParams)) {
181
                $requestParams['scope'] = implode(' ', $scopeParams);
182
            } elseif (is_string($scopeParams)) {
183
                //if scope is a string with ',' => make it to an array
184
                $requestParams['scope'] = str_replace(',', ' ', $scopeParams);
185
            }
186
        }
187
188
        return $urlGenerator->getUrl('www', 'uas/oauth2/authorization', $requestParams);
189
    }
190
191
    /**
192
     * Get the authorization code from the query parameters, if it exists,
193
     * and otherwise return null to signal no authorization code was
194
     * discovered.
195
     *
196
     * @return string|null The authorization code, or null if the authorization code not exists.
197
     *
198
     * @throws LinkedInException on invalid CSRF tokens
199
     */
200
    protected function getCode()
201
    {
202
        $storage = $this->getStorage();
203
204
        if (!GlobalVariableGetter::has('code')) {
205
            return;
206
        }
207
208
        if ($storage->get('code') === GlobalVariableGetter::get('code')) {
209
            //we have already validated this code
210
            return;
211
        }
212
213
        // if stored state does not exists
214
        if (null === $state = $storage->get('state')) {
215
            throw new LinkedInException('Could not find a stored CSRF state token.');
216
        }
217
218
        // if state not exists in the request
219
        if (!GlobalVariableGetter::has('state')) {
220
            throw new LinkedInException('Could not find a CSRF state token in the request.');
221
        }
222
223
        // if state exists in session and in request and if they are not equal
224
        if ($state !== GlobalVariableGetter::get('state')) {
225
            throw new LinkedInException('The CSRF state token from the request does not match the stored token.');
226
        }
227
228
        // CSRF state has done its job, so clear it
229
        $storage->clear('state');
230
231
        return GlobalVariableGetter::get('code');
232
    }
233
234
    /**
235
     * Lays down a CSRF state token for this process.
236
     */
237
    protected function establishCSRFTokenState()
238
    {
239
        $storage = $this->getStorage();
240
        if ($storage->get('state') === null) {
241
            $storage->set('state', md5(uniqid(mt_rand(), true)));
242
        }
243
    }
244
245
    /**
246
     * Clear the storage.
247
     *
248
     * @return $this
249
     */
250
    public function clearStorage()
251
    {
252
        $this->getStorage()->clearAll();
253
254
        return $this;
255
    }
256
257
    /**
258
     * @return DataStorageInterface
259
     */
260
    protected function getStorage()
261
    {
262
        if ($this->storage === null) {
263
            $this->storage = new SessionStorage();
264
        }
265
266
        return $this->storage;
267
    }
268
269
    /**
270
     * @param DataStorageInterface $storage
271
     *
272
     * @return $this
273
     */
274
    public function setStorage(DataStorageInterface $storage)
275
    {
276
        $this->storage = $storage;
277
278
        return $this;
279
    }
280
281
    /**
282
     * @return RequestManager
283
     */
284
    protected function getRequestManager()
285
    {
286
        return $this->requestManager;
287
    }
288
}
289