1<?php
2/**
3 * Copyright 2014 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 */
24namespace Facebook\Helpers;
25
26use Facebook\Authentication\AccessToken;
27use Facebook\Authentication\OAuth2Client;
28use Facebook\Url\UrlDetectionInterface;
29use Facebook\Url\FacebookUrlDetectionHandler;
30use Facebook\Url\FacebookUrlManipulator;
31use Facebook\PersistentData\PersistentDataInterface;
32use Facebook\PersistentData\FacebookSessionPersistentDataHandler;
33use Facebook\PseudoRandomString\PseudoRandomStringGeneratorInterface;
34use Facebook\PseudoRandomString\McryptPseudoRandomStringGenerator;
35use Facebook\PseudoRandomString\OpenSslPseudoRandomStringGenerator;
36use Facebook\PseudoRandomString\UrandomPseudoRandomStringGenerator;
37use Facebook\Exceptions\FacebookSDKException;
38
39/**
40 * Class FacebookRedirectLoginHelper
41 *
42 * @package Facebook
43 */
44class FacebookRedirectLoginHelper
45{
46    /**
47     * @const int The length of CSRF string to validate the login link.
48     */
49    const CSRF_LENGTH = 32;
50
51    /**
52     * @var OAuth2Client The OAuth 2.0 client service.
53     */
54    protected $oAuth2Client;
55
56    /**
57     * @var UrlDetectionInterface The URL detection handler.
58     */
59    protected $urlDetectionHandler;
60
61    /**
62     * @var PersistentDataInterface The persistent data handler.
63     */
64    protected $persistentDataHandler;
65
66    /**
67     * @var PseudoRandomStringGeneratorInterface The cryptographically secure pseudo-random string generator.
68     */
69    protected $pseudoRandomStringGenerator;
70
71    /**
72     * @param OAuth2Client                              $oAuth2Client          The OAuth 2.0 client service.
73     * @param PersistentDataInterface|null              $persistentDataHandler The persistent data handler.
74     * @param UrlDetectionInterface|null                $urlHandler            The URL detection handler.
75     * @param PseudoRandomStringGeneratorInterface|null $prsg                  The cryptographically secure pseudo-random string generator.
76     */
77    public function __construct(OAuth2Client $oAuth2Client, PersistentDataInterface $persistentDataHandler = null, UrlDetectionInterface $urlHandler = null, PseudoRandomStringGeneratorInterface $prsg = null)
78    {
79        $this->oAuth2Client = $oAuth2Client;
80        $this->persistentDataHandler = $persistentDataHandler ?: new FacebookSessionPersistentDataHandler();
81        $this->urlDetectionHandler = $urlHandler ?: new FacebookUrlDetectionHandler();
82        $this->pseudoRandomStringGenerator = $prsg ?: $this->detectPseudoRandomStringGenerator();
83    }
84
85    /**
86     * Returns the persistent data handler.
87     *
88     * @return PersistentDataInterface
89     */
90    public function getPersistentDataHandler()
91    {
92        return $this->persistentDataHandler;
93    }
94
95    /**
96     * Returns the URL detection handler.
97     *
98     * @return UrlDetectionInterface
99     */
100    public function getUrlDetectionHandler()
101    {
102        return $this->urlDetectionHandler;
103    }
104
105    /**
106     * Returns the cryptographically secure pseudo-random string generator.
107     *
108     * @return PseudoRandomStringGeneratorInterface
109     */
110    public function getPseudoRandomStringGenerator()
111    {
112        return $this->pseudoRandomStringGenerator;
113    }
114
115    /**
116     * Detects which pseudo-random string generator to use.
117     *
118     * @return PseudoRandomStringGeneratorInterface
119     *
120     * @throws FacebookSDKException
121     */
122    public function detectPseudoRandomStringGenerator()
123    {
124        // Since openssl_random_pseudo_bytes() can sometimes return non-cryptographically
125        // secure pseudo-random strings (in rare cases), we check for mcrypt_create_iv() first.
126        if (function_exists('mcrypt_create_iv')) {
127            return new McryptPseudoRandomStringGenerator();
128        }
129
130        if (function_exists('openssl_random_pseudo_bytes')) {
131            return new OpenSslPseudoRandomStringGenerator();
132        }
133
134        if (!ini_get('open_basedir') && is_readable('/dev/urandom')) {
135            return new UrandomPseudoRandomStringGenerator();
136        }
137
138        throw new FacebookSDKException('Unable to detect a cryptographically secure pseudo-random string generator.');
139    }
140
141    /**
142     * Stores CSRF state and returns a URL to which the user should be sent to in order to continue the login process with Facebook.
143     *
144     * @param string $redirectUrl The URL Facebook should redirect users to after login.
145     * @param array  $scope       List of permissions to request during login.
146     * @param array  $params      An array of parameters to generate URL.
147     * @param string $separator   The separator to use in http_build_query().
148     *
149     * @return string
150     */
151    private function makeUrl($redirectUrl, array $scope, array $params = [], $separator = '&')
152    {
153        $state = $this->pseudoRandomStringGenerator->getPseudoRandomString(static::CSRF_LENGTH);
154        $this->persistentDataHandler->set('state', $state);
155
156        return $this->oAuth2Client->getAuthorizationUrl($redirectUrl, $state, $scope, $params, $separator);
157    }
158
159    /**
160     * Returns the URL to send the user in order to login to Facebook.
161     *
162     * @param string $redirectUrl The URL Facebook should redirect users to after login.
163     * @param array  $scope       List of permissions to request during login.
164     * @param string $separator   The separator to use in http_build_query().
165     *
166     * @return string
167     */
168    public function getLoginUrl($redirectUrl, array $scope = [], $separator = '&')
169    {
170        return $this->makeUrl($redirectUrl, $scope, [], $separator);
171    }
172
173    /**
174     * Returns the URL to send the user in order to log out of Facebook.
175     *
176     * @param AccessToken|string $accessToken The access token that will be logged out.
177     * @param string             $next        The url Facebook should redirect the user to after a successful logout.
178     * @param string             $separator   The separator to use in http_build_query().
179     *
180     * @return string
181     *
182     * @throws FacebookSDKException
183     */
184    public function getLogoutUrl($accessToken, $next, $separator = '&')
185    {
186        if (!$accessToken instanceof AccessToken) {
187            $accessToken = new AccessToken($accessToken);
188        }
189
190        if ($accessToken->isAppAccessToken()) {
191            throw new FacebookSDKException('Cannot generate a logout URL with an app access token.', 722);
192        }
193
194        $params = [
195            'next' => $next,
196            'access_token' => $accessToken->getValue(),
197        ];
198
199        return 'https://www.facebook.com/logout.php?' . http_build_query($params, null, $separator);
200    }
201
202    /**
203     * Returns the URL to send the user in order to login to Facebook with permission(s) to be re-asked.
204     *
205     * @param string $redirectUrl The URL Facebook should redirect users to after login.
206     * @param array  $scope       List of permissions to request during login.
207     * @param string $separator   The separator to use in http_build_query().
208     *
209     * @return string
210     */
211    public function getReRequestUrl($redirectUrl, array $scope = [], $separator = '&')
212    {
213        $params = ['auth_type' => 'rerequest'];
214
215        return $this->makeUrl($redirectUrl, $scope, $params, $separator);
216    }
217
218    /**
219     * Returns the URL to send the user in order to login to Facebook with user to be re-authenticated.
220     *
221     * @param string $redirectUrl The URL Facebook should redirect users to after login.
222     * @param array  $scope       List of permissions to request during login.
223     * @param string $separator   The separator to use in http_build_query().
224     *
225     * @return string
226     */
227    public function getReAuthenticationUrl($redirectUrl, array $scope = [], $separator = '&')
228    {
229        $params = ['auth_type' => 'reauthenticate'];
230
231        return $this->makeUrl($redirectUrl, $scope, $params, $separator);
232    }
233
234    /**
235     * Takes a valid code from a login redirect, and returns an AccessToken entity.
236     *
237     * @param string|null $redirectUrl The redirect URL.
238     *
239     * @return AccessToken|null
240     *
241     * @throws FacebookSDKException
242     */
243    public function getAccessToken($redirectUrl = null)
244    {
245        if (!$code = $this->getCode()) {
246            return null;
247        }
248
249        $this->validateCsrf();
250
251        $redirectUrl = $redirectUrl ?: $this->urlDetectionHandler->getCurrentUrl();
252        // At minimum we need to remove the state param
253        $redirectUrl = FacebookUrlManipulator::removeParamsFromUrl($redirectUrl, ['state']);
254
255        return $this->oAuth2Client->getAccessTokenFromCode($code, $redirectUrl);
256    }
257
258    /**
259     * Validate the request against a cross-site request forgery.
260     *
261     * @throws FacebookSDKException
262     */
263    protected function validateCsrf()
264    {
265        $state = $this->getState();
266        $savedState = $this->persistentDataHandler->get('state');
267
268        if (!$state || !$savedState) {
269            throw new FacebookSDKException('Cross-site request forgery validation failed. Required param "state" missing.');
270        }
271
272        $savedLen = strlen($savedState);
273        $givenLen = strlen($state);
274
275        if ($savedLen !== $givenLen) {
276            throw new FacebookSDKException('Cross-site request forgery validation failed. The "state" param from the URL and session do not match.');
277        }
278
279        $result = 0;
280        for ($i = 0; $i < $savedLen; $i++) {
281            $result |= ord($state[$i]) ^ ord($savedState[$i]);
282        }
283
284        if ($result !== 0) {
285            throw new FacebookSDKException('Cross-site request forgery validation failed. The "state" param from the URL and session do not match.');
286        }
287    }
288
289    /**
290     * Return the code.
291     *
292     * @return string|null
293     */
294    protected function getCode()
295    {
296        return $this->getInput('code');
297    }
298
299    /**
300     * Return the state.
301     *
302     * @return string|null
303     */
304    protected function getState()
305    {
306        return $this->getInput('state');
307    }
308
309    /**
310     * Return the error code.
311     *
312     * @return string|null
313     */
314    public function getErrorCode()
315    {
316        return $this->getInput('error_code');
317    }
318
319    /**
320     * Returns the error.
321     *
322     * @return string|null
323     */
324    public function getError()
325    {
326        return $this->getInput('error');
327    }
328
329    /**
330     * Returns the error reason.
331     *
332     * @return string|null
333     */
334    public function getErrorReason()
335    {
336        return $this->getInput('error_reason');
337    }
338
339    /**
340     * Returns the error description.
341     *
342     * @return string|null
343     */
344    public function getErrorDescription()
345    {
346        return $this->getInput('error_description');
347    }
348
349    /**
350     * Returns a value from a GET param.
351     *
352     * @param string $key
353     *
354     * @return string|null
355     */
356    private function getInput($key)
357    {
358        return isset($_GET[$key]) ? $_GET[$key] : null;
359    }
360}
361