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

Authenticator::getCode()   B

Complexity

Conditions 6
Paths 6

Size

Total Lines 33
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 3
Bugs 0 Features 0
Metric Value
c 3
b 0
f 0
dl 0
loc 33
rs 8.439
cc 6
eloc 14
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
/**
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 UrlGeneratorInterface $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(UrlGeneratorInterface $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 UrlGeneratorInterface $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(UrlGeneratorInterface $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_url');
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 UrlGeneratorInterface $urlGenerator
156
     * @param array                 $options
157
     *
158
     * @return string
159
     */
160
    public function getLoginUrl(UrlGeneratorInterface $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
        ), $options);
171
172
        // Look for the redirect URL
173
        if (isset($options['redirect_uri'])) {
174
            $requestParams['redirect_uri'] = $options['redirect_uri'];
175
        } else {
176
            $requestParams['redirect_uri'] = $urlGenerator->getCurrentUrl();
177
        }
178
179
        // Save the redirect url for later
180
        $this->getStorage()->set('redirect_url', $requestParams['redirect_uri']);
181
182
        // if 'scope' is passed as an array, convert to space separated list
183
        $scopeParams = isset($options['scope']) ? $options['scope'] : null;
184
        if ($scopeParams) {
185
            //if scope is an array
186
            if (is_array($scopeParams)) {
187
                $requestParams['scope'] = implode(' ', $scopeParams);
188
            } elseif (is_string($scopeParams)) {
189
                //if scope is a string with ',' => make it to an array
190
                $requestParams['scope'] = str_replace(',', ' ', $scopeParams);
191
            }
192
        }
193
194
        return $urlGenerator->getUrl('www', 'uas/oauth2/authorization', $requestParams);
195
    }
196
197
    /**
198
     * Get the authorization code from the query parameters, if it exists,
199
     * and otherwise return null to signal no authorization code was
200
     * discovered.
201
     *
202
     * @return string|null The authorization code, or null if the authorization code not exists.
203
     *
204
     * @throws LinkedInException on invalid CSRF tokens
205
     */
206
    protected function getCode()
207
    {
208
        $storage = $this->getStorage();
209
210
        if (!GlobalVariableGetter::has('code')) {
211
            return;
212
        }
213
214
        if ($storage->get('code') === GlobalVariableGetter::get('code')) {
215
            //we have already validated this code
216
            return;
217
        }
218
219
        // if stored state does not exists
220
        if (null === $state = $storage->get('state')) {
221
            throw new LinkedInException('Could not find a stored CSRF state token.');
222
        }
223
224
        // if state not exists in the request
225
        if (!GlobalVariableGetter::has('state')) {
226
            throw new LinkedInException('Could not find a CSRF state token in the request.');
227
        }
228
229
        // if state exists in session and in request and if they are not equal
230
        if ($state !== GlobalVariableGetter::get('state')) {
231
            throw new LinkedInException('The CSRF state token from the request does not match the stored token.');
232
        }
233
234
        // CSRF state has done its job, so clear it
235
        $storage->clear('state');
236
237
        return GlobalVariableGetter::get('code');
238
    }
239
240
    /**
241
     * Lays down a CSRF state token for this process.
242
     */
243
    protected function establishCSRFTokenState()
244
    {
245
        $storage = $this->getStorage();
246
        if ($storage->get('state') === null) {
247
            $storage->set('state', md5(uniqid(mt_rand(), true)));
248
        }
249
    }
250
251
    /**
252
     * Clear the storage.
253
     *
254
     * @return $this
255
     */
256
    public function clearStorage()
257
    {
258
        $this->getStorage()->clearAll();
259
260
        return $this;
261
    }
262
263
    /**
264
     * @return DataStorageInterface
265
     */
266
    protected function getStorage()
267
    {
268
        if ($this->storage === null) {
269
            $this->storage = new SessionStorage();
270
        }
271
272
        return $this->storage;
273
    }
274
275
    /**
276
     * @param DataStorageInterface $storage
277
     *
278
     * @return $this
279
     */
280
    public function setStorage(DataStorageInterface $storage)
281
    {
282
        $this->storage = $storage;
283
284
        return $this;
285
    }
286
287
    /**
288
     * @return RequestManager
289
     */
290
    protected function getRequestManager()
291
    {
292
        return $this->requestManager;
293
    }
294
}
295