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