Skip to content
Snippets Groups Projects
Commit a7a9f4b0 authored by Andjelko Horvat's avatar Andjelko Horvat
Browse files

authfacebook: update extlibinc to 3.2.2.

git-svn-id: https://simplesamlphp.googlecode.com/svn/trunk@3292 44740490-163a-0410-bde0-09ae8108e29a
parent 9b5e9635
No related branches found
No related tags found
No related merge requests found
......@@ -120,7 +120,12 @@ abstract class BaseFacebook
/**
* Version.
*/
const VERSION = '3.1.1';
const VERSION = '3.2.2';
/**
* Signed Request Algorithm.
*/
const SIGNED_REQUEST_ALGORITHM = 'HMAC-SHA256';
/**
* Default options for curl.
......@@ -129,7 +134,7 @@ abstract class BaseFacebook
CURLOPT_CONNECTTIMEOUT => 10,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 60,
CURLOPT_USERAGENT => 'facebook-php-3.1',
CURLOPT_USERAGENT => 'facebook-php-3.2',
);
/**
......@@ -146,11 +151,12 @@ abstract class BaseFacebook
* Maps aliases to Facebook domains.
*/
public static $DOMAIN_MAP = array(
'api' => 'https://api.facebook.com/',
'api_video' => 'https://api-video.facebook.com/',
'api_read' => 'https://api-read.facebook.com/',
'graph' => 'https://graph.facebook.com/',
'www' => 'https://www.facebook.com/',
'api' => 'https://api.facebook.com/',
'api_video' => 'https://api-video.facebook.com/',
'api_read' => 'https://api-read.facebook.com/',
'graph' => 'https://graph.facebook.com/',
'graph_video' => 'https://graph-video.facebook.com/',
'www' => 'https://www.facebook.com/',
);
/**
......@@ -161,11 +167,11 @@ abstract class BaseFacebook
protected $appId;
/**
* The Application API Secret.
* The Application App Secret.
*
* @var string
*/
protected $apiSecret;
protected $appSecret;
/**
* The ID of the Facebook user, or 0 if the user is logged out.
......@@ -199,6 +205,13 @@ abstract class BaseFacebook
*/
protected $fileUploadSupport = false;
/**
* Indicates if we trust HTTP_X_FORWARDED_* headers.
*
* @var boolean
*/
protected $trustForwarded = false;
/**
* Initialize a Facebook Application.
*
......@@ -211,14 +224,16 @@ abstract class BaseFacebook
*/
public function __construct($config) {
$this->setAppId($config['appId']);
$this->setApiSecret($config['secret']);
$this->setAppSecret($config['secret']);
if (isset($config['fileUpload'])) {
$this->setFileUploadSupport($config['fileUpload']);
}
if (isset($config['trustForwarded']) && $config['trustForwarded']) {
$this->trustForwarded = true;
}
$state = $this->getPersistentData('state');
if (!empty($state)) {
$this->state = $this->getPersistentData('state');
$this->state = $state;
}
}
......@@ -243,23 +258,45 @@ abstract class BaseFacebook
}
/**
* Set the API Secret.
* Set the App Secret.
*
* @param string $apiSecret The API Secret
* @param string $apiSecret The App Secret
* @return BaseFacebook
* @deprecated
*/
public function setApiSecret($apiSecret) {
$this->apiSecret = $apiSecret;
$this->setAppSecret($apiSecret);
return $this;
}
/**
* Get the API Secret.
* Set the App Secret.
*
* @return string the API Secret
* @param string $appSecret The App Secret
* @return BaseFacebook
*/
public function setAppSecret($appSecret) {
$this->appSecret = $appSecret;
return $this;
}
/**
* Get the App Secret.
*
* @return string the App Secret
* @deprecated
*/
public function getApiSecret() {
return $this->apiSecret;
return $this->getAppSecret();
}
/**
* Get the App Secret.
*
* @return string the App Secret
*/
public function getAppSecret() {
return $this->appSecret;
}
/**
......@@ -278,10 +315,21 @@ abstract class BaseFacebook
*
* @return boolean true if and only if the server supports file upload.
*/
public function useFileUploadSupport() {
public function getFileUploadSupport() {
return $this->fileUploadSupport;
}
/**
* DEPRECATED! Please use getFileUploadSupport instead.
*
* Get the file upload support status.
*
* @return boolean true if and only if the server supports file upload.
*/
public function useFileUploadSupport() {
return $this->getFileUploadSupport();
}
/**
* Sets the access token for api calls. Use this if you get
* your access token by other means and just want the SDK
......@@ -295,6 +343,49 @@ abstract class BaseFacebook
return $this;
}
/**
* Extend an access token, while removing the short-lived token that might
* have been generated via client-side flow. Thanks to http://bit.ly/b0Pt0H
* for the workaround.
*/
public function setExtendedAccessToken() {
try {
// need to circumvent json_decode by calling _oauthRequest
// directly, since response isn't JSON format.
$access_token_response = $this->_oauthRequest(
$this->getUrl('graph', '/oauth/access_token'),
$params = array(
'client_id' => $this->getAppId(),
'client_secret' => $this->getAppSecret(),
'grant_type' => 'fb_exchange_token',
'fb_exchange_token' => $this->getAccessToken(),
)
);
}
catch (FacebookApiException $e) {
// most likely that user very recently revoked authorization.
// In any event, we don't have an access token, so say so.
return false;
}
if (empty($access_token_response)) {
return false;
}
$response_params = array();
parse_str($access_token_response, $response_params);
if (!isset($response_params['access_token'])) {
return false;
}
$this->destroySession();
$this->setPersistentData(
'access_token', $response_params['access_token']
);
}
/**
* Determines the access token that should be used for API calls.
* The first time this is called, $this->accessToken is set equal
......@@ -348,6 +439,11 @@ abstract class BaseFacebook
// the JS SDK puts a code in with the redirect_uri of ''
if (array_key_exists('code', $signed_request)) {
$code = $signed_request['code'];
if ($code && $code == $this->getPersistentData('code')) {
// short-circuit if the code we have is the same as the one presented
return $this->getPersistentData('access_token');
}
$access_token = $this->getAccessTokenFromCode($code, '');
if ($access_token) {
$this->setPersistentData('code', $code);
......@@ -392,10 +488,10 @@ abstract class BaseFacebook
*/
public function getSignedRequest() {
if (!$this->signedRequest) {
if (isset($_REQUEST['signed_request'])) {
if (!empty($_REQUEST['signed_request'])) {
$this->signedRequest = $this->parseSignedRequest(
$_REQUEST['signed_request']);
} else if (isset($_COOKIE[$this->getSignedRequestCookieName()])) {
} else if (!empty($_COOKIE[$this->getSignedRequestCookieName()])) {
$this->signedRequest = $this->parseSignedRequest(
$_COOKIE[$this->getSignedRequestCookieName()]);
}
......@@ -433,6 +529,11 @@ abstract class BaseFacebook
if ($signed_request) {
if (array_key_exists('user_id', $signed_request)) {
$user = $signed_request['user_id'];
if($user != $this->getPersistentData('user_id')){
$this->clearAllPersistentData();
}
$this->setPersistentData('user_id', $signed_request['user_id']);
return $user;
}
......@@ -510,7 +611,7 @@ abstract class BaseFacebook
'logout.php',
array_merge(array(
'next' => $this->getCurrentUrl(),
'access_token' => $this->getAccessToken(),
'access_token' => $this->getUserAccessToken(),
), $params)
);
}
......@@ -567,6 +668,17 @@ abstract class BaseFacebook
return 'fbsr_'.$this->getAppId();
}
/**
* Constructs and returns the name of the coookie that potentially contain
* metadata. The cookie is not set by the BaseFacebook class, but it may be
* set by the JavaScript SDK.
*
* @return string the name of the cookie that would house metadata.
*/
protected function getMetadataCookieName() {
return 'fbm_'.$this->getAppId();
}
/**
* Get the authorization code from the query parameters, if it exists,
* and otherwise return false to signal no authorization code was
......@@ -621,7 +733,7 @@ abstract class BaseFacebook
* public information about users and applications.
*/
protected function getApplicationAccessToken() {
return $this->appId.'|'.$this->apiSecret;
return $this->appId.'|'.$this->appSecret;
}
/**
......@@ -664,7 +776,7 @@ abstract class BaseFacebook
$this->_oauthRequest(
$this->getUrl('graph', '/oauth/access_token'),
$params = array('client_id' => $this->getAppId(),
'client_secret' => $this->getApiSecret(),
'client_secret' => $this->getAppSecret(),
'redirect_uri' => $redirect_uri,
'code' => $code));
} catch (FacebookApiException $e) {
......@@ -707,16 +819,34 @@ abstract class BaseFacebook
// results are returned, errors are thrown
if (is_array($result) && isset($result['error_code'])) {
$this->throwAPIException($result);
// @codeCoverageIgnoreStart
}
// @codeCoverageIgnoreEnd
if ($params['method'] === 'auth.expireSession' ||
$params['method'] === 'auth.revokeAuthorization') {
$method = strtolower($params['method']);
if ($method === 'auth.expiresession' ||
$method === 'auth.revokeauthorization') {
$this->destroySession();
}
return $result;
}
/**
* Return true if this is video post.
*
* @param string $path The path
* @param string $method The http method (default 'GET')
*
* @return boolean true if this is video post
*/
protected function isVideoPost($path, $method = 'GET') {
if ($method == 'POST' && preg_match("/^(\/)(.+)(\/)(videos)$/", $path)) {
return true;
}
return false;
}
/**
* Invoke the Graph API.
*
......@@ -734,15 +864,23 @@ abstract class BaseFacebook
}
$params['method'] = $method; // method override as we always do a POST
if ($this->isVideoPost($path, $method)) {
$domainKey = 'graph_video';
} else {
$domainKey = 'graph';
}
$result = json_decode($this->_oauthRequest(
$this->getUrl('graph', $path),
$this->getUrl($domainKey, $path),
$params
), true);
// results are returned, errors are thrown
if (is_array($result) && isset($result['error'])) {
$this->throwAPIException($result);
// @codeCoverageIgnoreStart
}
// @codeCoverageIgnoreEnd
return $result;
}
......@@ -788,7 +926,7 @@ abstract class BaseFacebook
}
$opts = self::$CURL_OPTS;
if ($this->useFileUploadSupport()) {
if ($this->getFileUploadSupport()) {
$opts[CURLOPT_POSTFIELDS] = $params;
} else {
$opts[CURLOPT_POSTFIELDS] = http_build_query($params, null, '&');
......@@ -816,6 +954,25 @@ abstract class BaseFacebook
$result = curl_exec($ch);
}
// With dual stacked DNS responses, it's possible for a server to
// have IPv6 enabled but not have IPv6 connectivity. If this is
// the case, curl will try IPv4 first and if that fails, then it will
// fall back to IPv6 and the error EHOSTUNREACH is returned by the
// operating system.
if ($result === false && empty($opts[CURLOPT_IPRESOLVE])) {
$matches = array();
$regex = '/Failed to connect to ([^:].*): Network is unreachable/';
if (preg_match($regex, curl_error($ch), $matches)) {
if (strlen(@inet_pton($matches[1])) === 16) {
self::errorLog('Invalid IPv6 configuration on server, '.
'Please disable or get native IPv6 on your server.');
self::$CURL_OPTS[CURLOPT_IPRESOLVE] = CURL_IPRESOLVE_V4;
curl_setopt($ch, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4);
$result = curl_exec($ch);
}
}
}
if ($result === false) {
$e = new FacebookApiException(array(
'error_code' => curl_errno($ch),
......@@ -844,14 +1001,15 @@ abstract class BaseFacebook
$sig = self::base64UrlDecode($encoded_sig);
$data = json_decode(self::base64UrlDecode($payload), true);
if (strtoupper($data['algorithm']) !== 'HMAC-SHA256') {
self::errorLog('Unknown algorithm. Expected HMAC-SHA256');
if (strtoupper($data['algorithm']) !== self::SIGNED_REQUEST_ALGORITHM) {
self::errorLog(
'Unknown algorithm. Expected ' . self::SIGNED_REQUEST_ALGORITHM);
return null;
}
// check sig
$expected_sig = hash_hmac('sha256', $payload,
$this->getApiSecret(), $raw = true);
$this->getAppSecret(), $raw = true);
if ($sig !== $expected_sig) {
self::errorLog('Bad Signed JSON signature!');
return null;
......@@ -860,6 +1018,26 @@ abstract class BaseFacebook
return $data;
}
/**
* Makes a signed_request blob using the given data.
*
* @param array The data array.
* @return string The signed request.
*/
protected function makeSignedRequest($data) {
if (!is_array($data)) {
throw new InvalidArgumentException(
'makeSignedRequest expects an array. Got: ' . print_r($data, true));
}
$data['algorithm'] = self::SIGNED_REQUEST_ALGORITHM;
$data['issued_at'] = time();
$json = json_encode($data);
$b64 = self::base64UrlEncode($json);
$raw_sig = hash_hmac('sha256', $b64, $this->getAppSecret(), $raw = true);
$sig = self::base64UrlEncode($raw_sig);
return $sig.'.'.$b64;
}
/**
* Build the URL for api given parameters.
*
......@@ -961,6 +1139,49 @@ abstract class BaseFacebook
return $url;
}
protected function getHttpHost() {
if ($this->trustForwarded && isset($_SERVER['HTTP_X_FORWARDED_HOST'])) {
return $_SERVER['HTTP_X_FORWARDED_HOST'];
}
return $_SERVER['HTTP_HOST'];
}
protected function getHttpProtocol() {
if ($this->trustForwarded && isset($_SERVER['HTTP_X_FORWARDED_PROTO'])) {
if ($_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https') {
return 'https';
}
return 'http';
}
/*apache + variants specific way of checking for https*/
if (isset($_SERVER['HTTPS']) &&
($_SERVER['HTTPS'] === 'on' || $_SERVER['HTTPS'] == 1)) {
return 'https';
}
/*nginx way of checking for https*/
if (isset($_SERVER['SERVER_PORT']) &&
($_SERVER['SERVER_PORT'] === '443')) {
return 'https';
}
return 'http';
}
/**
* Get the base domain used for the cookie.
*/
protected function getBaseDomain() {
// The base domain is stored in the metadata cookie if not we fallback
// to the current hostname
$metadata = $this->getMetadataCookie();
if (array_key_exists('base_domain', $metadata) &&
!empty($metadata['base_domain'])) {
return trim($metadata['base_domain'], '.');
}
return $this->getHttpHost();
}
/**
/**
* Returns the Current URL, stripping it of known FB parameters that should
* not persist.
......@@ -968,15 +1189,9 @@ abstract class BaseFacebook
* @return string The current URL
*/
protected function getCurrentUrl() {
if (isset($_SERVER['HTTPS']) && ($_SERVER['HTTPS'] == 'on' || $_SERVER['HTTPS'] == 1)
|| isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] == 'https'
) {
$protocol = 'https://';
}
else {
$protocol = 'http://';
}
$currentUrl = $protocol . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'];
$protocol = $this->getHttpProtocol() . '://';
$host = $this->getHttpHost();
$currentUrl = $protocol.$host.$_SERVER['REQUEST_URI'];
$parts = parse_url($currentUrl);
$query = '';
......@@ -1030,7 +1245,7 @@ abstract class BaseFacebook
/**
* Analyzes the supplied result to see if it was thrown
* because the access token is no longer valid. If that is
* the case, then the persistent store is cleared.
* the case, then we destroy the session.
*
* @param $result array A record storing the error message returned
* by a failed API call.
......@@ -1045,12 +1260,13 @@ abstract class BaseFacebook
// REST server errors are just Exceptions
case 'Exception':
$message = $e->getMessage();
if ((strpos($message, 'Error validating access token') !== false) ||
(strpos($message, 'Invalid OAuth access token') !== false)) {
$this->setAccessToken(null);
$this->user = 0;
$this->clearAllPersistentData();
}
if ((strpos($message, 'Error validating access token') !== false) ||
(strpos($message, 'Invalid OAuth access token') !== false) ||
(strpos($message, 'An active access token must be used') !== false)
) {
$this->destroySession();
}
break;
}
throw $e;
......@@ -1078,6 +1294,7 @@ abstract class BaseFacebook
* Exactly the same as base64_encode except it uses
* - instead of +
* _ instead of /
* No padded =
*
* @param string $input base64UrlEncoded string
* @return string
......@@ -1086,13 +1303,94 @@ abstract class BaseFacebook
return base64_decode(strtr($input, '-_', '+/'));
}
/**
* Base64 encoding that doesn't need to be urlencode()ed.
* Exactly the same as base64_encode except it uses
* - instead of +
* _ instead of /
*
* @param string $input string
* @return string base64Url encoded string
*/
protected static function base64UrlEncode($input) {
$str = strtr(base64_encode($input), '+/', '-_');
$str = str_replace('=', '', $str);
return $str;
}
/**
* Destroy the current session
*/
public function destroySession() {
$this->setAccessToken(null);
$this->user = 0;
$this->accessToken = null;
$this->signedRequest = null;
$this->user = null;
$this->clearAllPersistentData();
// Javascript sets a cookie that will be used in getSignedRequest that we
// need to clear if we can
$cookie_name = $this->getSignedRequestCookieName();
if (array_key_exists($cookie_name, $_COOKIE)) {
unset($_COOKIE[$cookie_name]);
if (!headers_sent()) {
$base_domain = $this->getBaseDomain();
setcookie($cookie_name, '', 1, '/', '.'.$base_domain);
} else {
// @codeCoverageIgnoreStart
self::errorLog(
'There exists a cookie that we wanted to clear that we couldn\'t '.
'clear because headers was already sent. Make sure to do the first '.
'API call before outputing anything.'
);
// @codeCoverageIgnoreEnd
}
}
}
/**
* Parses the metadata cookie that our Javascript API set
*
* @return an array mapping key to value
*/
protected function getMetadataCookie() {
$cookie_name = $this->getMetadataCookieName();
if (!array_key_exists($cookie_name, $_COOKIE)) {
return array();
}
// The cookie value can be wrapped in "-characters so remove them
$cookie_value = trim($_COOKIE[$cookie_name], '"');
if (empty($cookie_value)) {
return array();
}
$parts = explode('&', $cookie_value);
$metadata = array();
foreach ($parts as $part) {
$pair = explode('=', $part, 2);
if (!empty($pair[0])) {
$metadata[urldecode($pair[0])] =
(count($pair) > 1) ? urldecode($pair[1]) : '';
}
}
return $metadata;
}
protected static function isAllowedDomain($big, $small) {
if ($big === $small) {
return true;
}
return self::endsWith($big, '.'.$small);
}
protected static function endsWith($big, $small) {
$len = strlen($small);
if ($len === 0) {
return true;
}
return substr($big, -$len) === $small;
}
/**
......
This diff is collapsed.
......@@ -8,6 +8,14 @@ require_once(dirname(dirname(__FILE__)) . '/extlibinc/base_facebook.php');
*/
class sspmod_authfacebook_Facebook extends BaseFacebook
{
const FBSS_COOKIE_NAME = 'fbss';
// We can set this to a high number because the main session
// expiration will trump this.
const FBSS_COOKIE_EXPIRE = 31556926; // 1 year
// Stores the shared session ID if one is set.
protected $sharedSessionID;
/* SimpleSAMLPhp state array */
protected $ssp_state;
......@@ -18,18 +26,60 @@ class sspmod_authfacebook_Facebook extends BaseFacebook
* access token if during the course of execution
* we discover them.
*
* @param Array $config the application configuration.
* @param Array $config the application configuration. Additionally
* accepts "sharedSession" as a boolean to turn on a secondary
* cookie for environments with a shared session (that is, your app
* shares the domain with other apps).
* @see BaseFacebook::__construct in base_facebook.php
*/
public function __construct(array $config, &$ssp_state) {
$this->ssp_state = &$ssp_state;
parent::__construct($config);
if (!empty($config['sharedSession'])) {
$this->initSharedSession();
}
}
protected static $kSupportedKeys =
array('state', 'code', 'access_token', 'user_id');
protected function initSharedSession() {
$cookie_name = $this->getSharedSessionCookieName();
if (isset($_COOKIE[$cookie_name])) {
$data = $this->parseSignedRequest($_COOKIE[$cookie_name]);
if ($data && !empty($data['domain']) &&
self::isAllowedDomain($this->getHttpHost(), $data['domain'])) {
// good case
$this->sharedSessionID = $data['id'];
return;
}
// ignoring potentially unreachable data
}
// evil/corrupt/missing case
$base_domain = $this->getBaseDomain();
$this->sharedSessionID = md5(uniqid(mt_rand(), true));
$cookie_value = $this->makeSignedRequest(
array(
'domain' => $base_domain,
'id' => $this->sharedSessionID,
)
);
$_COOKIE[$cookie_name] = $cookie_value;
if (!headers_sent()) {
$expire = time() + self::FBSS_COOKIE_EXPIRE;
setcookie($cookie_name, $cookie_value, $expire, '/', '.'.$base_domain);
} else {
// @codeCoverageIgnoreStart
SimpleSAML_Logger::debug(
'Shared session ID cookie could not be set! You must ensure you '.
'create the Facebook instance before headers have been sent. This '.
'will cause authentication issues after the first request.'
);
// @codeCoverageIgnoreEnd
}
}
/**
* Provides the implementations of the inherited abstract
* methods. The implementation uses PHP sessions to maintain
......@@ -53,10 +103,8 @@ class sspmod_authfacebook_Facebook extends BaseFacebook
}
$session_var_name = $this->constructSessionVariableName($key);
if (isset($this->ssp_state[$session_var_name])) {
$value = $this->ssp_state[$session_var_name];
}
return isset($value) ? $value : $default;
return isset($this->ssp_state[$session_var_name]) ?
$this->ssp_state[$session_var_name] : $default;
}
protected function clearPersistentData($key) {
......@@ -75,11 +123,27 @@ class sspmod_authfacebook_Facebook extends BaseFacebook
foreach (self::$kSupportedKeys as $key) {
$this->clearPersistentData($key);
}
if ($this->sharedSessionID) {
$this->deleteSharedSessionCookie();
}
}
protected function deleteSharedSessionCookie() {
$cookie_name = $this->getSharedSessionCookieName();
unset($_COOKIE[$cookie_name]);
$base_domain = $this->getBaseDomain();
setcookie($cookie_name, '', 1, '/', '.'.$base_domain);
}
protected function getSharedSessionCookieName() {
return self::FBSS_COOKIE_NAME . '_' . $this->getAppId();
}
protected function constructSessionVariableName($key) {
return 'authfacebook:authdata:' . implode('_', array('fb',
$this->getAppId(),
$key));
$parts = array('authfacebook:authdata:fb', $this->getAppId(), $key);
if ($this->sharedSessionID) {
array_unshift($parts, $this->sharedSessionID);
}
return implode('_', $parts);
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment