Completed
Push — master ( b65b29...f39982 )
by Tobias
03:16 queued 01:01
created

Authenticator::establishCSRFTokenState()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
c 2
b 0
f 0
dl 0
loc 7
rs 9.4286
cc 2
eloc 4
nc 2
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\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 RequestManager
43
     */
44
    private $requestManager;
45
46
    /**
47
     * @param RequestManagerInterface $requestManager
48
     * @param string                  $appId
49
     * @param string                  $appSecret
50
     */
51
    public function __construct(RequestManagerInterface $requestManager, $appId, $appSecret)
52
    {
53
        $this->appId = $appId;
54
        $this->appSecret = $appSecret;
55
        $this->requestManager = $requestManager;
0 ignored issues
show
Documentation Bug introduced by
$requestManager is of type object<Happyr\LinkedIn\H...equestManagerInterface>, but the property $requestManager was declared to be of type object<Happyr\LinkedIn\Http\RequestManager>. Are you sure that you always receive this specific sub-class here, or does it make sense to add an instanceof check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a given class or a super-class is assigned to a property that is type hinted more strictly.

Either this assignment is in error or an instanceof check should be added for that assignment.

class Alien {}

class Dalek extends Alien {}

class Plot
{
    /** @var  Dalek */
    public $villain;
}

$alien = new Alien();
$plot = new Plot();
if ($alien instanceof Dalek) {
    $plot->villain = $alien;
}
Loading history...
56
    }
57
58
    /**
59
     * {@inheritdoc}
60
     */
61
    public function fetchNewAccessToken(LinkedInUrlGeneratorInterface $urlGenerator)
62
    {
63
        $storage = $this->getStorage();
64
        $code = $this->getCode();
65
66
        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
            return $storage->get('access_token');
73
        }
74
75
        try {
76
            $accessToken = $this->getAccessTokenFromCode($urlGenerator, $code);
77
        } catch (LinkedInException $e) {
78
            // code was bogus, so everything based on it should be invalidated.
79
            $storage->clearAll();
80
            throw $e;
81
        }
82
83
        $storage->set('code', $code);
84
        $storage->set('access_token', $accessToken);
85
86
        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
    protected function getAccessTokenFromCode(LinkedInUrlGeneratorInterface $urlGenerator, $code)
105
    {
106
        if (empty($code)) {
107
            throw new LinkedInException('Could not get access token: The code was empty.');
108
        }
109
110
        $redirectUri = $this->getStorage()->get('redirect_uri');
111
        try {
112
            $url = $urlGenerator->getUrl('www', 'uas/oauth2/accessToken');
113
            $headers = ['Content-Type' => 'application/x-www-form-urlencoded'];
114
            $body = http_build_query(
115
                [
116
                    'grant_type' => 'authorization_code',
117
                    'code' => $code,
118
                    'redirect_uri' => $redirectUri,
119
                    'client_id' => $this->appId,
120
                    'client_secret' => $this->appSecret,
121
                ]
122
            );
123
124
            $response = ResponseConverter::convertToArray($this->getRequestManager()->sendRequest('POST', $url, $headers, $body));
125
        } 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
        if (empty($response)) {
132
            throw new LinkedInException('Could not get access token: The response from LinkedIn.com was empty.');
133
        }
134
135
        $tokenData = array_merge(array('access_token' => null, 'expires_in' => null), $response);
136
        $token = new AccessToken($tokenData['access_token'], $tokenData['expires_in']);
137
138
        if (!$token->hasToken()) {
139
            throw new LinkedInException('Could not get access token: The response from LinkedIn.com did not contain a token.');
140
        }
141
142
        return $token;
143
    }
144
145
    /**
146
     * {@inheritdoc}
147
     */
148
    public function getLoginUrl(LinkedInUrlGeneratorInterface $urlGenerator, $options = array())
149
    {
150
        // Generate a state
151
        $this->establishCSRFTokenState();
152
153
        // Build request params
154
        $requestParams = array_merge(array(
155
            'response_type' => 'code',
156
            'client_id' => $this->appId,
157
            'state' => $this->getStorage()->get('state'),
158
            'redirect_uri' => null,
159
        ), $options);
160
161
        // Save the redirect url for later
162
        $this->getStorage()->set('redirect_uri', $requestParams['redirect_uri']);
163
164
        // if 'scope' is passed as an array, convert to space separated list
165
        $scopeParams = isset($options['scope']) ? $options['scope'] : null;
166
        if ($scopeParams) {
167
            //if scope is an array
168
            if (is_array($scopeParams)) {
169
                $requestParams['scope'] = implode(' ', $scopeParams);
170
            } elseif (is_string($scopeParams)) {
171
                //if scope is a string with ',' => make it to an array
172
                $requestParams['scope'] = str_replace(',', ' ', $scopeParams);
173
            }
174
        }
175
176
        return $urlGenerator->getUrl('www', 'uas/oauth2/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
    protected function getCode()
189
    {
190
        $storage = $this->getStorage();
191
192
        if (!GlobalVariableGetter::has('code')) {
193
            return;
194
        }
195
196
        if ($storage->get('code') === GlobalVariableGetter::get('code')) {
197
            //we have already validated this code
198
            return;
199
        }
200
201
        // if stored state does not exists
202
        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
        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
        if ($state !== GlobalVariableGetter::get('state')) {
213
            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
        $storage->clear('state');
218
219
        return GlobalVariableGetter::get('code');
220
    }
221
222
    /**
223
     * Lays down a CSRF state token for this process.
224
     */
225
    protected function establishCSRFTokenState()
226
    {
227
        $storage = $this->getStorage();
228
        if ($storage->get('state') === null) {
229
            $storage->set('state', md5(uniqid(mt_rand(), true)));
230
        }
231
    }
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
    protected function getStorage()
247
    {
248
        if ($this->storage === null) {
249
            $this->storage = new SessionStorage();
250
        }
251
252
        return $this->storage;
253
    }
254
255
    /**
256
     * {@inheritdoc}
257
     */
258
    public function setStorage(DataStorageInterface $storage)
259
    {
260
        $this->storage = $storage;
261
262
        return $this;
263
    }
264
265
    /**
266
     * @return RequestManager
267
     */
268
    protected function getRequestManager()
269
    {
270
        return $this->requestManager;
271
    }
272
}
273