Completed
Push — master ( 7113c4...8875d3 )
by Yassine
14s
created

RedirectLoginHelper::validateCsrf()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 16
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 4.432

Importance

Changes 0
Metric Value
cc 4
eloc 9
nc 4
nop 0
dl 0
loc 16
ccs 7
cts 10
cp 0.7
crap 4.432
rs 9.2
c 0
b 0
f 0
1
<?php
2
/**
3
 * Copyright 2017 Facebook, Inc.
4
 *
5
 * You are hereby granted a non-exclusive, worldwide, royalty-free license to
6
 * use, copy, modify, and distribute this software in source code or binary
7
 * form for use in connection with the web services and APIs provided by
8
 * Facebook.
9
 *
10
 * As with any software that integrates with the Facebook platform, your use
11
 * of this software is subject to the Facebook Developer Principles and
12
 * Policies [http://developers.facebook.com/policy/]. This copyright notice
13
 * shall be included in all copies or substantial portions of the software.
14
 *
15
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
18
 * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
20
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
21
 * DEALINGS IN THE SOFTWARE.
22
 */
23
namespace Facebook\Helper;
24
25
use Facebook\Authentication\AccessToken;
26
use Facebook\Authentication\OAuth2Client;
27
use Facebook\Exception\SDKException;
28
use Facebook\PersistentData\SessionPersistentDataHandler;
29
use Facebook\PersistentData\PersistentDataInterface;
30
use Facebook\Url\UrlDetectionHandler;
31
use Facebook\Url\UrlManipulator;
32
use Facebook\Url\UrlDetectionInterface;
33
34
/**
35
 * @package Facebook
36
 */
37
class RedirectLoginHelper
38
{
39
    /**
40
     * @const int The length of CSRF string to validate the login link.
41
     */
42
    const CSRF_LENGTH = 32;
43
44
    /**
45
     * @var OAuth2Client The OAuth 2.0 client service.
46
     */
47
    protected $oAuth2Client;
48
49
    /**
50
     * @var UrlDetectionInterface the URL detection handler
51
     */
52
    protected $urlDetectionHandler;
53
54
    /**
55
     * @var PersistentDataInterface the persistent data handler
56
     */
57
    protected $persistentDataHandler;
58
59
    /**
60
     * @param OAuth2Client                 $oAuth2Client          The OAuth 2.0 client service.
61
     * @param null|PersistentDataInterface $persistentDataHandler the persistent data handler
62
     * @param null|UrlDetectionInterface   $urlHandler            the URL detection handler
63
     */
64 5
    public function __construct(OAuth2Client $oAuth2Client, PersistentDataInterface $persistentDataHandler = null, UrlDetectionInterface $urlHandler = null)
65
    {
66 5
        $this->oAuth2Client = $oAuth2Client;
67 5
        $this->persistentDataHandler = $persistentDataHandler ?: new SessionPersistentDataHandler();
68 5
        $this->urlDetectionHandler = $urlHandler ?: new UrlDetectionHandler();
69 5
    }
70
71
    /**
72
     * Returns the persistent data handler.
73
     *
74
     * @return PersistentDataInterface
75
     */
76 2
    public function getPersistentDataHandler()
77
    {
78 2
        return $this->persistentDataHandler;
79
    }
80
81
    /**
82
     * Returns the URL detection handler.
83
     *
84
     * @return UrlDetectionInterface
85
     */
86 1
    public function getUrlDetectionHandler()
87
    {
88 1
        return $this->urlDetectionHandler;
89
    }
90
91
    /**
92
     * Stores CSRF state and returns a URL to which the user should be sent to in order to continue the login process with Facebook.
93
     *
94
     * @param string $redirectUrl the URL Facebook should redirect users to after login
95
     * @param array  $scope       list of permissions to request during login
96
     * @param array  $params      an array of parameters to generate URL
97
     * @param string $separator   the separator to use in http_build_query()
98
     *
99
     * @return string
100
     */
101 1
    private function makeUrl($redirectUrl, array $scope, array $params = [], $separator = '&')
102
    {
103 1
        $state = $this->persistentDataHandler->get('state') ?: $this->getPseudoRandomString();
104 1
        $this->persistentDataHandler->set('state', $state);
105
106 1
        return $this->oAuth2Client->getAuthorizationUrl($redirectUrl, $state, $scope, $params, $separator);
107
    }
108
109 1
    private function getPseudoRandomString()
110
    {
111 1
        return bin2hex(random_bytes(static::CSRF_LENGTH));
112
    }
113
114
    /**
115
     * Returns the URL to send the user in order to login to Facebook.
116
     *
117
     * @param string $redirectUrl the URL Facebook should redirect users to after login
118
     * @param array  $scope       list of permissions to request during login
119
     * @param string $separator   the separator to use in http_build_query()
120
     *
121
     * @return string
122
     */
123 1
    public function getLoginUrl($redirectUrl, array $scope = [], $separator = '&')
124
    {
125 1
        return $this->makeUrl($redirectUrl, $scope, [], $separator);
126
    }
127
128
    /**
129
     * Returns the URL to send the user in order to log out of Facebook.
130
     *
131
     * @param AccessToken|string $accessToken the access token that will be logged out
132
     * @param string             $next        the url Facebook should redirect the user to after a successful logout
133
     * @param string             $separator   the separator to use in http_build_query()
134
     *
135
     * @throws SDKException
136
     *
137
     * @return string
138
     */
139 1
    public function getLogoutUrl($accessToken, $next, $separator = '&')
140
    {
141 1
        if (!$accessToken instanceof AccessToken) {
142 1
            $accessToken = new AccessToken($accessToken);
143
        }
144
145 1
        if ($accessToken->isAppAccessToken()) {
146
            throw new SDKException('Cannot generate a logout URL with an app access token.', 722);
147
        }
148
149
        $params = [
150 1
            'next' => $next,
151 1
            'access_token' => $accessToken->getValue(),
152
        ];
153
154 1
        return 'https://www.facebook.com/logout.php?' . http_build_query($params, null, $separator);
155
    }
156
157
    /**
158
     * Returns the URL to send the user in order to login to Facebook with permission(s) to be re-asked.
159
     *
160
     * @param string $redirectUrl the URL Facebook should redirect users to after login
161
     * @param array  $scope       list of permissions to request during login
162
     * @param string $separator   the separator to use in http_build_query()
163
     *
164
     * @return string
165
     */
166
    public function getReRequestUrl($redirectUrl, array $scope = [], $separator = '&')
167
    {
168
        $params = ['auth_type' => 'rerequest'];
169
170
        return $this->makeUrl($redirectUrl, $scope, $params, $separator);
171
    }
172
173
    /**
174
     * Returns the URL to send the user in order to login to Facebook with user to be re-authenticated.
175
     *
176
     * @param string $redirectUrl the URL Facebook should redirect users to after login
177
     * @param array  $scope       list of permissions to request during login
178
     * @param string $separator   the separator to use in http_build_query()
179
     *
180
     * @return string
181
     */
182
    public function getReAuthenticationUrl($redirectUrl, array $scope = [], $separator = '&')
183
    {
184
        $params = ['auth_type' => 'reauthenticate'];
185
186
        return $this->makeUrl($redirectUrl, $scope, $params, $separator);
187
    }
188
189
    /**
190
     * Takes a valid code from a login redirect, and returns an AccessToken entity.
191
     *
192
     * @param null|string $redirectUrl the redirect URL
193
     *
194
     * @throws SDKException
195
     *
196
     * @return null|AccessToken
197
     */
198 1
    public function getAccessToken($redirectUrl = null)
199
    {
200 1
        if (!$code = $this->getCode()) {
201
            return null;
202
        }
203
204 1
        $this->validateCsrf();
205 1
        $this->resetCsrf();
206
207 1
        $redirectUrl = $redirectUrl ?: $this->urlDetectionHandler->getCurrentUrl();
208
        // At minimum we need to remove the state param
209 1
        $redirectUrl = UrlManipulator::removeParamsFromUrl($redirectUrl, ['state']);
210
211 1
        return $this->oAuth2Client->getAccessTokenFromCode($code, $redirectUrl);
212
    }
213
214
    /**
215
     * Validate the request against a cross-site request forgery.
216
     *
217
     * @throws SDKException
218
     */
219 1
    protected function validateCsrf()
220
    {
221 1
        $state = $this->getState();
222 1
        if (!$state) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $state of type null|string is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
223
            throw new SDKException('Cross-site request forgery validation failed. Required GET param "state" missing.');
224
        }
225 1
        $savedState = $this->persistentDataHandler->get('state');
226 1
        if (!$savedState) {
227
            throw new SDKException('Cross-site request forgery validation failed. Required param "state" missing from persistent data.');
228
        }
229
230 1
        if (\hash_equals($savedState, $state)) {
231 1
            return;
232
        }
233
234
        throw new SDKException('Cross-site request forgery validation failed. The "state" param from the URL and session do not match.');
235
    }
236
237
    /**
238
     * Resets the CSRF so that it doesn't get reused.
239
     */
240 1
    private function resetCsrf()
241
    {
242 1
        $this->persistentDataHandler->set('state', null);
243 1
    }
244
245
    /**
246
     * Return the code.
247
     *
248
     * @return null|string
249
     */
250 1
    protected function getCode()
251
    {
252 1
        return $this->getInput('code');
253
    }
254
255
    /**
256
     * Return the state.
257
     *
258
     * @return null|string
259
     */
260 1
    protected function getState()
261
    {
262 1
        return $this->getInput('state');
263
    }
264
265
    /**
266
     * Return the error code.
267
     *
268
     * @return null|string
269
     */
270
    public function getErrorCode()
271
    {
272
        return $this->getInput('error_code');
273
    }
274
275
    /**
276
     * Returns the error.
277
     *
278
     * @return null|string
279
     */
280
    public function getError()
281
    {
282
        return $this->getInput('error');
283
    }
284
285
    /**
286
     * Returns the error reason.
287
     *
288
     * @return null|string
289
     */
290
    public function getErrorReason()
291
    {
292
        return $this->getInput('error_reason');
293
    }
294
295
    /**
296
     * Returns the error description.
297
     *
298
     * @return null|string
299
     */
300
    public function getErrorDescription()
301
    {
302
        return $this->getInput('error_description');
303
    }
304
305
    /**
306
     * Returns a value from a GET param.
307
     *
308
     * @param string $key
309
     *
310
     * @return null|string
311
     */
312 1
    private function getInput($key)
313
    {
314 1
        return isset($_GET[$key]) ? $_GET[$key] : null;
315
    }
316
}
317