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