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