Authenticator   A
last analyzed

Complexity

Total Complexity 27

Size/Duplication

Total Lines 255
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 8

Test Coverage

Coverage 91.21%

Importance

Changes 0
Metric Value
wmc 27
lcom 1
cbo 8
dl 0
loc 255
ccs 83
cts 91
cp 0.9121
rs 10
c 0
b 0
f 0

10 Methods

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