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

authfacebook: update to OAuth 2.0 SDK (issue 410).

git-svn-id: https://simplesamlphp.googlecode.com/svn/trunk@2866 44740490-163a-0410-bde0-09ae8108e29a
parent efbb3355
Branches
Tags
No related merge requests found
<?php
/**
*
* Copyright 2011 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may
......@@ -38,7 +37,7 @@ class FacebookApiException extends Exception
/**
* Make a new API Exception with the given result.
*
* @param Array $result the result from the API server
* @param array $result The result from the API server
*/
public function __construct($result) {
$this->result = $result;
......@@ -64,7 +63,7 @@ class FacebookApiException extends Exception
/**
* Return the associated result object returned by the API server.
*
* @returns Array the result from the API server
* @return array The result from the API server
*/
public function getResult() {
return $this->result;
......@@ -74,7 +73,7 @@ class FacebookApiException extends Exception
* Returns the associated type for the error. This will default to
* 'Exception' when a type is not available.
*
* @return String
* @return string
*/
public function getType() {
if (isset($this->result['error'])) {
......@@ -89,13 +88,14 @@ class FacebookApiException extends Exception
}
}
}
return 'Exception';
}
/**
* To make debugging easier.
*
* @returns String the string representation of the error
* @return string The string representation of the error
*/
public function __toString() {
$str = $this->getType() . ': ';
......@@ -107,16 +107,20 @@ class FacebookApiException extends Exception
}
/**
* Provides access to the Facebook Platform.
* Provides access to the Facebook Platform. This class provides
* a majority of the functionality needed, but the class is abstract
* because it is designed to be sub-classed. The subclass must
* implement the three abstract methods listed at the bottom of
* the file.
*
* @author Naitik Shah <naitik@facebook.com>
*/
class Facebook
abstract class BaseFacebook
{
/**
* Version.
*/
const VERSION = '2.1.2';
const VERSION = '3.0.1';
/**
* Default options for curl.
......@@ -125,7 +129,7 @@ class Facebook
CURLOPT_CONNECTTIMEOUT => 10,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 60,
CURLOPT_USERAGENT => 'facebook-php-2.0',
CURLOPT_USERAGENT => 'facebook-php-3.0',
);
/**
......@@ -133,7 +137,8 @@ class Facebook
* the current URL.
*/
protected static $DROP_QUERY_PARAMS = array(
'session',
'code',
'state',
'signed_request',
);
......@@ -150,18 +155,24 @@ class Facebook
/**
* The Application ID.
*
* @var string
*/
protected $appId;
/**
* The Application API Secret.
*
* @var string
*/
protected $apiSecret;
/**
* The active user session, if one is available.
* The ID of the Facebook user, or 0 if the user is logged out.
*
* @var integer
*/
protected $session;
protected $user;
/**
* The data from the signed_request token.
......@@ -169,22 +180,22 @@ class Facebook
protected $signedRequest;
/**
* Indicates that we already loaded the session as best as we could.
* A CSRF state variable to assist in the defense against CSRF attacks.
*/
protected $sessionLoaded = false;
protected $state;
/**
* Indicates if Cookie support should be enabled.
*/
protected $cookieSupport = false;
/**
* Base domain for the Cookie.
* The OAuth access token received in exchange for a valid authorization
* code. null means the access token has yet to be determined.
*
* @var string
*/
protected $baseDomain = '';
protected $accessToken = null;
/**
* Indicates if the CURL based @ syntax for file uploads is enabled.
*
* @var boolean
*/
protected $fileUploadSupport = false;
......@@ -194,30 +205,28 @@ class Facebook
* The configuration:
* - appId: the application ID
* - secret: the application secret
* - cookie: (optional) boolean true to enable cookie support
* - domain: (optional) domain for the cookie
* - fileUpload: (optional) boolean indicating if file uploads are enabled
*
* @param Array $config the application configuration
* @param array $config The application configuration
*/
public function __construct($config) {
$this->setAppId($config['appId']);
$this->setApiSecret($config['secret']);
if (isset($config['cookie'])) {
$this->setCookieSupport($config['cookie']);
}
if (isset($config['domain'])) {
$this->setBaseDomain($config['domain']);
}
if (isset($config['fileUpload'])) {
$this->setFileUploadSupport($config['fileUpload']);
}
$state = $this->getPersistentData('state');
if (!empty($state)) {
$this->state = $this->getPersistentData('state');
}
}
/**
* Set the Application ID.
*
* @param String $appId the Application ID
* @param string $appId The Application ID
* @return BaseFacebook
*/
public function setAppId($appId) {
$this->appId = $appId;
......@@ -227,7 +236,7 @@ class Facebook
/**
* Get the Application ID.
*
* @return String the Application ID
* @return string the Application ID
*/
public function getAppId() {
return $this->appId;
......@@ -236,7 +245,8 @@ class Facebook
/**
* Set the API Secret.
*
* @param String $appId the API Secret
* @param string $apiSecret The API Secret
* @return BaseFacebook
*/
public function setApiSecret($apiSecret) {
$this->apiSecret = $apiSecret;
......@@ -246,73 +256,125 @@ class Facebook
/**
* Get the API Secret.
*
* @return String the API Secret
* @return string the API Secret
*/
public function getApiSecret() {
return $this->apiSecret;
}
/**
* Set the Cookie Support status.
* Set the file upload support status.
*
* @param Boolean $cookieSupport the Cookie Support status
* @param boolean $fileUploadSupport The file upload support status.
* @return BaseFacebook
*/
public function setCookieSupport($cookieSupport) {
$this->cookieSupport = $cookieSupport;
public function setFileUploadSupport($fileUploadSupport) {
$this->fileUploadSupport = $fileUploadSupport;
return $this;
}
/**
* Get the Cookie Support status.
* Get the file upload support status.
*
* @return Boolean the Cookie Support status
* @return boolean true if and only if the server supports file upload.
*/
public function useCookieSupport() {
return $this->cookieSupport;
public function useFileUploadSupport() {
return $this->fileUploadSupport;
}
/**
* Set the base domain for the Cookie.
* Sets the access token for api calls. Use this if you get
* your access token by other means and just want the SDK
* to use it.
*
* @param String $domain the base domain
* @param string $access_token an access token.
* @return BaseFacebook
*/
public function setBaseDomain($domain) {
$this->baseDomain = $domain;
public function setAccessToken($access_token) {
$this->accessToken = $access_token;
return $this;
}
/**
* Get the base domain for the Cookie.
* Determines the access token that should be used for API calls.
* The first time this is called, $this->accessToken is set equal
* to either a valid user access token, or it's set to the application
* access token if a valid user access token wasn't available. Subsequent
* calls return whatever the first call returned.
*
* @return String the base domain
* @return string The access token
*/
public function getBaseDomain() {
return $this->baseDomain;
}
public function getAccessToken() {
if ($this->accessToken !== null) {
// we've done this already and cached it. Just return.
return $this->accessToken;
}
/**
* Set the file upload support status.
*
* @param String $domain the base domain
*/
public function setFileUploadSupport($fileUploadSupport) {
$this->fileUploadSupport = $fileUploadSupport;
return $this;
// first establish access token to be the application
// access token, in case we navigate to the /oauth/access_token
// endpoint, where SOME access token is required.
$this->setAccessToken($this->getApplicationAccessToken());
if ($user_access_token = $this->getUserAccessToken()) {
$this->setAccessToken($user_access_token);
}
return $this->accessToken;
}
/**
* Get the file upload support status.
* Determines and returns the user access token, first using
* the signed request if present, and then falling back on
* the authorization code if present. The intent is to
* return a valid user access token, or false if one is determined
* to not be available.
*
* @return String the base domain
* @return string A valid user access token, or false if one
* could not be determined.
*/
public function useFileUploadSupport() {
return $this->fileUploadSupport;
protected function getUserAccessToken() {
// first, consider a signed request if it's supplied.
// if there is a signed request, then it alone determines
// the access token.
$signed_request = $this->getSignedRequest();
if ($signed_request) {
if (array_key_exists('oauth_token', $signed_request)) {
$access_token = $signed_request['oauth_token'];
$this->setPersistentData('access_token', $access_token);
return $access_token;
}
// signed request states there's no access token, so anything
// stored should be cleared.
$this->clearAllPersistentData();
return false; // respect the signed request's data, even
// if there's an authorization code or something else
}
$code = $this->getCode();
if ($code && $code != $this->getPersistentData('code')) {
$access_token = $this->getAccessTokenFromCode($code);
if ($access_token) {
$this->setPersistentData('code', $code);
$this->setPersistentData('access_token', $access_token);
return $access_token;
}
// code was bogus, so everything based on it should be invalidated.
$this->clearAllPersistentData();
return false;
}
// as a fallback, just return whatever is in the persistent
// store, knowing nothing explicit (signed request, authorization
// code, etc.) was present to shadow it (or we saw a code in $_REQUEST,
// but it's the same as what's in the persistent store)
return $this->getPersistentData('access_token');
}
/**
* Get the data from a signed_request token
* Get the data from a signed_request token.
*
* @return String the base domain
* @return string The base domain
*/
public function getSignedRequest() {
if (!$this->signedRequest) {
......@@ -325,97 +387,63 @@ class Facebook
}
/**
* Set the Session.
* Get the UID of the connected user, or 0
* if the Facebook user is not connected.
*
* @param Array $session the session
* @param Boolean $write_cookie indicate if a cookie should be written. this
* value is ignored if cookie support has been disabled.
* @return string the UID if available.
*/
public function setSession($session=null, $write_cookie=true) {
$session = $this->validateSessionObject($session);
$this->sessionLoaded = true;
$this->session = $session;
if ($write_cookie) {
$this->setCookieFromSession($session);
public function getUser() {
if ($this->user !== null) {
// we've already determined this and cached the value.
return $this->user;
}
return $this;
return $this->user = $this->getUserFromAvailableData();
}
/**
* Get the session object. This will automatically look for a signed session
* sent via the signed_request, Cookie or Query Parameters if needed.
* Determines the connected user by first examining any signed
* requests, then considering an authorization code, and then
* falling back to any persistent store storing the user.
*
* @return Array the session
* @return integer The id of the connected Facebook user,
* or 0 if no such user exists.
*/
public function getSession() {
if (!$this->sessionLoaded) {
$session = null;
$write_cookie = true;
// try loading session from signed_request in $_REQUEST
$signedRequest = $this->getSignedRequest();
if ($signedRequest) {
// sig is good, use the signedRequest
$session = $this->createSessionFromSignedRequest($signedRequest);
protected function getUserFromAvailableData() {
// if a signed request is supplied, then it solely determines
// who the user is.
$signed_request = $this->getSignedRequest();
if ($signed_request) {
if (array_key_exists('user_id', $signed_request)) {
$user = $signed_request['user_id'];
$this->setPersistentData('user_id', $signed_request['user_id']);
return $user;
}
// try loading session from $_REQUEST
if (!$session && isset($_REQUEST['session'])) {
$session = json_decode(
get_magic_quotes_gpc()
? stripslashes($_REQUEST['session'])
: $_REQUEST['session'],
true
);
$session = $this->validateSessionObject($session);
}
// if the signed request didn't present a user id, then invalidate
// all entries in any persistent store.
$this->clearAllPersistentData();
return 0;
}
// try loading session from cookie if necessary
if (!$session && $this->useCookieSupport()) {
$cookieName = $this->getSessionCookieName();
if (isset($_COOKIE[$cookieName])) {
$session = array();
parse_str(trim(
get_magic_quotes_gpc()
? stripslashes($_COOKIE[$cookieName])
: $_COOKIE[$cookieName],
'"'
), $session);
$session = $this->validateSessionObject($session);
// write only if we need to delete a invalid session cookie
$write_cookie = empty($session);
}
$user = $this->getPersistentData('user_id', $default = 0);
$persisted_access_token = $this->getPersistentData('access_token');
// use access_token to fetch user id if we have a user access_token, or if
// the cached access token has changed.
$access_token = $this->getAccessToken();
if ($access_token &&
$access_token != $this->getApplicationAccessToken() &&
!($user && $persisted_access_token == $access_token)) {
$user = $this->getUserFromAccessToken();
if ($user) {
$this->setPersistentData('user_id', $user);
} else {
$this->clearAllPersistentData();
}
$this->setSession($session, $write_cookie);
}
return $this->session;
}
/**
* Get the UID from the session.
*
* @return String the UID if available
*/
public function getUser() {
$session = $this->getSession();
return $session ? $session['uid'] : null;
}
/**
* Gets a OAuth access token.
*
* @return String the access token
*/
public function getAccessToken() {
$session = $this->getSession();
// either user session signed, or app signed
if ($session) {
return $session['access_token'];
} else {
return $this->getAppId() .'|'. $this->getApiSecret();
}
return $user;
}
/**
......@@ -424,30 +452,23 @@ class Facebook
* JavaScript, you can pass in display=popup as part of the $params.
*
* The parameters:
* - next: the url to go to after a successful login
* - cancel_url: the url to go to after the user cancels
* - req_perms: comma separated list of requested extended perms
* - display: can be "page" (default, full page) or "popup"
* - redirect_uri: the url to go to after a successful login
* - scope: comma separated list of requested extended perms
*
* @param Array $params provide custom parameters
* @return String the URL for the login flow
* @param array $params Provide custom parameters
* @return string The URL for the login flow
*/
public function getLoginUrl($params=array()) {
$this->establishCSRFTokenState();
$currentUrl = $this->getCurrentUrl();
return $this->getUrl(
'www',
'login.php',
'dialog/oauth',
array_merge(array(
'api_key' => $this->getAppId(),
'cancel_url' => $currentUrl,
'display' => 'page',
'fbconnect' => 1,
'next' => $currentUrl,
'return_session' => 1,
'session_version' => 3,
'v' => '1.0',
), $params)
);
'client_id' => $this->getAppId(),
'redirect_uri' => $currentUrl, // possibly overwritten
'state' => $this->state),
$params));
}
/**
......@@ -456,40 +477,40 @@ class Facebook
* The parameters:
* - next: the url to go to after a successful logout
*
* @param Array $params provide custom parameters
* @return String the URL for the logout flow
* @param array $params Provide custom parameters
* @return string The URL for the logout flow
*/
public function getLogoutUrl($params=array()) {
return $this->getUrl(
'www',
'logout.php',
array_merge(array(
'next' => $this->getCurrentUrl(),
'next' => $this->getCurrentUrl(),
'access_token' => $this->getAccessToken(),
), $params)
);
}
/**
* Get a login status URL to fetch the status from facebook.
* Get a login status URL to fetch the status from Facebook.
*
* The parameters:
* - ok_session: the URL to go to if a session is found
* - no_session: the URL to go to if the user is not connected
* - no_user: the URL to go to if the user is not signed into facebook
*
* @param Array $params provide custom parameters
* @return String the URL for the logout flow
* @param array $params Provide custom parameters
* @return string The URL for the logout flow
*/
public function getLoginStatusUrl($params=array()) {
return $this->getUrl(
'www',
'extern/login_status.php',
array_merge(array(
'api_key' => $this->getAppId(),
'no_session' => $this->getCurrentUrl(),
'no_user' => $this->getCurrentUrl(),
'ok_session' => $this->getCurrentUrl(),
'api_key' => $this->getAppId(),
'no_session' => $this->getCurrentUrl(),
'no_user' => $this->getCurrentUrl(),
'ok_session' => $this->getCurrentUrl(),
'session_version' => 3,
), $params)
);
......@@ -498,8 +519,7 @@ class Facebook
/**
* Make an API call.
*
* @param Array $params the API call parameters
* @return the decoded response
* @return mixed The decoded response
*/
public function api(/* polymorphic */) {
$args = func_get_args();
......@@ -510,11 +530,127 @@ class Facebook
}
}
/**
* Get the authorization code from the query parameters, if it exists,
* and otherwise return false to signal no authorization code was
* discoverable.
*
* @return mixed The authorization code, or false if the authorization
* code could not be determined.
*/
protected function getCode() {
if (isset($_REQUEST['code'])) {
if ($this->state !== null &&
isset($_REQUEST['state']) &&
$this->state === $_REQUEST['state']) {
// CSRF state has done its job, so clear it
$this->state = null;
$this->clearPersistentData('state');
return $_REQUEST['code'];
} else {
self::errorLog('CSRF state token does not match one provided.');
return false;
}
}
return false;
}
/**
* Retrieves the UID with the understanding that
* $this->accessToken has already been set and is
* seemingly legitimate. It relies on Facebook's Graph API
* to retrieve user information and then extract
* the user ID.
*
* @return integer Returns the UID of the Facebook user, or 0
* if the Facebook user could not be determined.
*/
protected function getUserFromAccessToken() {
try {
$user_info = $this->api('/me');
return $user_info['id'];
} catch (FacebookApiException $e) {
return 0;
}
}
/**
* Returns the access token that should be used for logged out
* users when no authorization code is available.
*
* @return string The application access token, useful for gathering
* public information about users and applications.
*/
protected function getApplicationAccessToken() {
return $this->appId.'|'.$this->apiSecret;
}
/**
* Lays down a CSRF state token for this process.
*
* @return void
*/
protected function establishCSRFTokenState() {
if ($this->state === null) {
$this->state = md5(uniqid(mt_rand(), true));
$this->setPersistentData('state', $this->state);
}
}
/**
* Retrieves an access token for the given authorization code
* (previously generated from www.facebook.com on behalf of
* a specific user). The authorization code is sent to graph.facebook.com
* and a legitimate access token is generated provided the access token
* and the user for which it was generated all match, and the user is
* either logged in to Facebook or has granted an offline access permission.
*
* @param string $code An authorization code.
* @return mixed An access token exchanged for the authorization code, or
* false if an access token could not be generated.
*/
protected function getAccessTokenFromCode($code) {
if (empty($code)) {
return false;
}
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->getApiSecret(),
'redirect_uri' => $this->getCurrentUrl(),
'code' => $code));
} 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;
}
return $response_params['access_token'];
}
/**
* Invoke the old restserver.php endpoint.
*
* @param Array $params method call object
* @return the decoded response object
* @param array $params Method call object
*
* @return mixed The decoded response object
* @throws FacebookApiException
*/
protected function _restserver($params) {
......@@ -531,19 +667,21 @@ class Facebook
if (is_array($result) && isset($result['error_code'])) {
throw new FacebookApiException($result);
}
return $result;
}
/**
* Invoke the Graph API.
*
* @param String $path the path (required)
* @param String $method the http method (default 'GET')
* @param Array $params the query/post data
* @return the decoded response object
* @param string $path The path (required)
* @param string $method The http method (default 'GET')
* @param array $params The query/post data
*
* @return mixed The decoded response object
* @throws FacebookApiException
*/
protected function _graph($path, $method='GET', $params=array()) {
protected function _graph($path, $method = 'GET', $params = array()) {
if (is_array($method) && empty($params)) {
$params = $method;
$method = 'GET';
......@@ -557,25 +695,19 @@ class Facebook
// results are returned, errors are thrown
if (is_array($result) && isset($result['error'])) {
$e = new FacebookApiException($result);
switch ($e->getType()) {
// OAuth 2.0 Draft 00 style
case 'OAuthException':
// OAuth 2.0 Draft 10 style
case 'invalid_token':
$this->setSession(null);
}
throw $e;
$this->throwAPIException($result);
}
return $result;
}
/**
* Make a OAuth Request
* Make a OAuth Request.
*
* @param string $url The path (required)
* @param array $params The query/post data
*
* @param String $path the path (required)
* @param Array $params the query/post data
* @return the decoded response object
* @return string The decoded response object
* @throws FacebookApiException
*/
protected function _oauthRequest($url, $params) {
......@@ -589,18 +721,20 @@ class Facebook
$params[$key] = json_encode($value);
}
}
return $this->makeRequest($url, $params);
}
/**
* Makes an HTTP request. This method can be overriden by subclasses if
* Makes an HTTP request. This method can be overridden by subclasses if
* developers want to do fancier things or use something other than curl to
* make the request.
*
* @param String $url the URL to make the request to
* @param Array $params the parameters to use for the POST body
* @param CurlHandler $ch optional initialized curl handle
* @return String the response text
* @param string $url The URL to make the request to
* @param array $params The parameters to use for the POST body
* @param CurlHandler $ch Initialized curl handle
*
* @return string The response text
*/
protected function makeRequest($url, $params, $ch=null) {
if (!$ch) {
......@@ -629,7 +763,8 @@ class Facebook
$result = curl_exec($ch);
if (curl_errno($ch) == 60) { // CURLE_SSL_CACERT
self::errorLog('Invalid or no certificate authority found, using bundled information');
self::errorLog('Invalid or no certificate authority found, '.
'using bundled information');
curl_setopt($ch, CURLOPT_CAINFO,
dirname(__FILE__) . '/fb_ca_chain_bundle.crt');
$result = curl_exec($ch);
......@@ -638,9 +773,9 @@ class Facebook
if ($result === false) {
$e = new FacebookApiException(array(
'error_code' => curl_errno($ch),
'error' => array(
'message' => curl_error($ch),
'type' => 'CurlException',
'error' => array(
'message' => curl_error($ch),
'type' => 'CurlException',
),
));
curl_close($ch);
......@@ -650,127 +785,11 @@ class Facebook
return $result;
}
/**
* The name of the Cookie that contains the session.
*
* @return String the cookie name
*/
protected function getSessionCookieName() {
return 'fbs_' . $this->getAppId();
}
/**
* Set a JS Cookie based on the _passed in_ session. It does not use the
* currently stored session -- you need to explicitly pass it in.
*
* @param Array $session the session to use for setting the cookie
*/
protected function setCookieFromSession($session=null) {
if (!$this->useCookieSupport()) {
return;
}
$cookieName = $this->getSessionCookieName();
$value = 'deleted';
$expires = time() - 3600;
$domain = $this->getBaseDomain();
if ($session) {
$value = '"' . http_build_query($session, null, '&') . '"';
if (isset($session['base_domain'])) {
$domain = $session['base_domain'];
}
$expires = $session['expires'];
}
// prepend dot if a domain is found
if ($domain) {
$domain = '.' . $domain;
}
// if an existing cookie is not set, we dont need to delete it
if ($value == 'deleted' && empty($_COOKIE[$cookieName])) {
return;
}
if (headers_sent()) {
self::errorLog('Could not set cookie. Headers already sent.');
// ignore for code coverage as we will never be able to setcookie in a CLI
// environment
// @codeCoverageIgnoreStart
} else {
setcookie($cookieName, $value, $expires, '/', $domain);
}
// @codeCoverageIgnoreEnd
}
/**
* Validates a session_version=3 style session object.
*
* @param Array $session the session object
* @return Array the session object if it validates, null otherwise
*/
protected function validateSessionObject($session) {
// make sure some essential fields exist
if (is_array($session) &&
isset($session['uid']) &&
isset($session['access_token']) &&
isset($session['sig'])) {
// validate the signature
$session_without_sig = $session;
unset($session_without_sig['sig']);
$expected_sig = self::generateSignature(
$session_without_sig,
$this->getApiSecret()
);
if ($session['sig'] != $expected_sig) {
self::errorLog('Got invalid session signature in cookie.');
$session = null;
}
// check expiry time
} else {
$session = null;
}
return $session;
}
/**
* Returns something that looks like our JS session object from the
* signed token's data
*
* TODO: Nuke this once the login flow uses OAuth2
*
* @param Array the output of getSignedRequest
* @return Array Something that will work as a session
*/
protected function createSessionFromSignedRequest($data) {
if (!isset($data['oauth_token'])) {
return null;
}
$session = array(
'uid' => $data['user_id'],
'access_token' => $data['oauth_token'],
'expires' => $data['expires'],
);
// put a real sig, so that validateSignature works
$session['sig'] = self::generateSignature(
$session,
$this->getApiSecret()
);
return $session;
}
/**
* Parses a signed_request and validates the signature.
* Then saves it in $this->signed_data
*
* @param String A signed token
* @param Boolean Should we remove the parts of the payload that
* are used by the algorithm?
* @return Array the payload inside it or null if the sig is wrong
* @param string $signed_request A signed token
* @return array The payload inside it or null if the sig is wrong
*/
protected function parseSignedRequest($signed_request) {
list($encoded_sig, $payload) = explode('.', $signed_request, 2);
......@@ -799,7 +818,7 @@ class Facebook
* Build the URL for api given parameters.
*
* @param $method String the method name.
* @return String the URL for the given parameters
* @return string The URL for the given parameters
*/
protected function getApiUrl($method) {
static $READ_ONLY_CALLS =
......@@ -875,10 +894,11 @@ class Facebook
/**
* Build the URL for given domain alias, path and parameters.
*
* @param $name String the name of the domain
* @param $path String optional path (without a leading slash)
* @param $params Array optional query parameters
* @return String the URL for the given parameters
* @param $name string The name of the domain
* @param $path string Optional path (without a leading slash)
* @param $params array Optional query parameters
*
* @return string The URL for the given parameters
*/
protected function getUrl($name, $path='', $params=array()) {
$url = self::$DOMAIN_MAP[$name];
......@@ -891,6 +911,7 @@ class Facebook
if ($params) {
$url .= '?' . http_build_query($params, null, '&');
}
return $url;
}
......@@ -898,7 +919,7 @@ class Facebook
* Returns the Current URL, stripping it of known FB parameters that should
* not persist.
*
* @return String the current URL
* @return string The current URL
*/
protected function getCurrentUrl() {
$protocol = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on'
......@@ -907,16 +928,19 @@ class Facebook
$currentUrl = $protocol . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'];
$parts = parse_url($currentUrl);
// drop known fb params
$query = '';
if (!empty($parts['query'])) {
$params = array();
parse_str($parts['query'], $params);
foreach(self::$DROP_QUERY_PARAMS as $key) {
unset($params[$key]);
// drop known fb params
$params = explode('&', $parts['query']);
$retained_params = array();
foreach ($params as $param) {
if ($this->shouldRetainParam($param)) {
$retained_params[] = $param;
}
}
if (!empty($params)) {
$query = '?' . http_build_query($params, null, '&');
if (!empty($retained_params)) {
$query = '?'.implode($retained_params, '&');
}
}
......@@ -932,30 +956,58 @@ class Facebook
}
/**
* Generate a signature for the given params and secret.
* Returns true if and only if the key or key/value pair should
* be retained as part of the query string. This amounts to
* a brute-force search of the very small list of Facebook-specific
* params that should be stripped out.
*
* @param string $param A key or key/value pair within a URL's query (e.g.
* 'foo=a', 'foo=', or 'foo'.
*
* @param Array $params the parameters to sign
* @param String $secret the secret to sign with
* @return String the generated signature
* @return boolean
*/
protected static function generateSignature($params, $secret) {
// work with sorted data
ksort($params);
protected function shouldRetainParam($param) {
foreach (self::$DROP_QUERY_PARAMS as $drop_query_param) {
if (strpos($param, $drop_query_param.'=') === 0) {
return false;
}
}
return true;
}
// generate the base string
$base_string = '';
foreach($params as $key => $value) {
$base_string .= $key . '=' . $value;
/**
* 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.
*
* @param $result array A record storing the error message returned
* by a failed API call.
*/
protected function throwAPIException($result) {
$e = new FacebookApiException($result);
switch ($e->getType()) {
// OAuth 2.0 Draft 00 style
case 'OAuthException':
// OAuth 2.0 Draft 10 style
case 'invalid_token':
$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();
}
}
$base_string .= $secret;
return md5($base_string);
throw $e;
}
/**
* Prints to the error log if you aren't in command line mode.
*
* @param String log message
* @param string $msg Log message
*/
protected static function errorLog($msg) {
// disable error log if we are running in a CLI environment
......@@ -974,9 +1026,56 @@ class Facebook
* - instead of +
* _ instead of /
*
* @param String base64UrlEncodeded string
* @param string $input base64UrlEncoded string
* @return string
*/
protected static function base64UrlDecode($input) {
return base64_decode(strtr($input, '-_', '+/'));
}
/**
* Each of the following four methods should be overridden in
* a concrete subclass, as they are in the provided Facebook class.
* The Facebook class uses PHP sessions to provide a primitive
* persistent store, but another subclass--one that you implement--
* might use a database, memcache, or an in-memory cache.
*
* @see Facebook
*/
/**
* Stores the given ($key, $value) pair, so that future calls to
* getPersistentData($key) return $value. This call may be in another request.
*
* @param string $key
* @param array $value
*
* @return void
*/
abstract protected function setPersistentData($key, $value);
/**
* Get the data for $key, persisted by BaseFacebook::setPersistentData()
*
* @param string $key The key of the data to retrieve
* @param boolean $default The default value to return if $key is not found
*
* @return mixed
*/
abstract protected function getPersistentData($key, $default = false);
/**
* Clear the data with $key from the persistent storage
*
* @param string $key
* @return void
*/
abstract protected function clearPersistentData($key);
/**
* Clear all data from the persistent storage
*
* @return void
*/
abstract protected function clearAllPersistentData();
}
......@@ -58,8 +58,6 @@ class sspmod_authfacebook_Auth_Source_Facebook extends SimpleSAML_Auth_Source {
$this->api_key = $cfgParse->getString('api_key');
$this->secret = $cfgParse->getString('secret');
$this->req_perms = $cfgParse->getString('req_perms', NULL);
require_once(dirname(dirname(dirname(dirname(__FILE__)))) . '/extlibinc/facebook.php');
}
......@@ -73,37 +71,35 @@ class sspmod_authfacebook_Auth_Source_Facebook extends SimpleSAML_Auth_Source {
/* We are going to need the authId in order to retrieve this authentication source later. */
$state[self::AUTHID] = $this->authId;
$stateID = SimpleSAML_Auth_State::saveState($state, self::STAGE_INIT);
SimpleSAML_Logger::debug('facebook auth state id = ' . $stateID);
$facebook = new sspmod_authfacebook_Facebook(array('appId' => $this->api_key, 'secret' => $this->secret), $state);
$facebook->clearAllPersistentData();
$linkback = SimpleSAML_Module::getModuleURL('authfacebook/linkback.php', array('AuthState' => $stateID));
$url = $facebook->getLoginUrl(array('redirect_uri' => $linkback, 'scope' => $this->req_perms));
SimpleSAML_Auth_State::saveState($state, self::STAGE_INIT);
SimpleSAML_Utilities::redirect($url);
}
$linkback = SimpleSAML_Module::getModuleURL('authfacebook/linkback.php');
$linkback_next = $linkback . '?next=' . urlencode($stateID);
$linkback_cancel = $linkback . '?cancel=' . urlencode($stateID);
$fb_login_params = array('next' => $linkback_next, 'cancel_url' => $linkback_cancel, 'req_perms' => $this->req_perms);
$facebook = new Facebook(array('appId' => $this->api_key, 'secret' => $this->secret, 'cookie' => false));
public function finalStep(&$state) {
assert('is_array($state)');
$fb_session = $facebook->getSession();
$facebook = new sspmod_authfacebook_Facebook(array('appId' => $this->api_key, 'secret' => $this->secret), $state);
$uid = $facebook->getUser();
if (isset($fb_session)) {
if (isset($uid)) {
try {
$uid = $facebook->getUser();
if (isset($uid)) {
$info = $facebook->api("/me");
}
$info = $facebook->api("/me");
} catch (FacebookApiException $e) {
if ($e->getType() != 'OAuthException') {
throw new SimpleSAML_Error_AuthSource($this->authId, 'Error getting user profile.', $e);
}
throw new SimpleSAML_Error_AuthSource($this->authId, 'Error getting user profile.', $e);
}
}
if (!isset($info)) {
$url = $facebook->getLoginUrl($fb_login_params);
SimpleSAML_Utilities::redirect($url);
assert('FALSE');
throw new SimpleSAML_Error_AuthSource($this->authId, 'Error getting user profile.');
}
$attributes = array();
......@@ -125,8 +121,9 @@ class sspmod_authfacebook_Auth_Source_Facebook extends SimpleSAML_Auth_Source {
SimpleSAML_Logger::debug('Facebook Returned Attributes: '. implode(", ", array_keys($attributes)));
$state['Attributes'] = $attributes;
}
$facebook->clearAllPersistentData();
}
}
......
<?php
require_once(dirname(dirname(__FILE__)) . '/extlibinc/base_facebook.php');
/**
* Extends the BaseFacebook class with the intent of using
* PHP sessions to store user ids and access tokens.
*/
class sspmod_authfacebook_Facebook extends BaseFacebook
{
/* SimpleSAMLPhp state array */
protected $ssp_state;
/**
* Identical to the parent constructor, except that
* we start a PHP session to store the user ID and
* access token if during the course of execution
* we discover them.
*
* @param Array $config the application configuration.
* @see BaseFacebook::__construct in base_facebook.php
*/
public function __construct(array $config, &$ssp_state) {
$this->ssp_state = &$ssp_state;
parent::__construct($config);
}
protected static $kSupportedKeys =
array('state', 'code', 'access_token', 'user_id');
/**
* Provides the implementations of the inherited abstract
* methods. The implementation uses PHP sessions to maintain
* a store for authorization codes, user ids, CSRF states, and
* access tokens.
*/
protected function setPersistentData($key, $value) {
if (!in_array($key, self::$kSupportedKeys)) {
SimpleSAML_Logger::debug("Unsupported key passed to setPersistentData: " . var_export($key, TRUE));
return;
}
$session_var_name = $this->constructSessionVariableName($key);
$this->ssp_state[$session_var_name] = $value;
}
protected function getPersistentData($key, $default = false) {
if (!in_array($key, self::$kSupportedKeys)) {
SimpleSAML_Logger::debug("Unsupported key passed to getPersistentData: " . var_export($key, TRUE));
return $default;
}
$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;
}
protected function clearPersistentData($key) {
if (!in_array($key, self::$kSupportedKeys)) {
SimpleSAML_Logger::debug("Unsupported key passed to clearPersistentData: " . var_export($key, TRUE));
return;
}
$session_var_name = $this->constructSessionVariableName($key);
if (isset($this->ssp_state[$session_var_name])) {
unset($this->ssp_state[$session_var_name]);
}
}
public function clearAllPersistentData() {
foreach (self::$kSupportedKeys as $key) {
$this->clearPersistentData($key);
}
}
protected function constructSessionVariableName($key) {
return 'authfacebook:authdata:' . implode('_', array('fb',
$this->getAppId(),
$key));
}
}
......@@ -4,18 +4,11 @@
* Handle linkback() response from Facebook.
*/
$cancel = FALSE;
if (array_key_exists('cancel', $_GET)) {
$stateID = $_GET['cancel'];
$cancel = TRUE;
} elseif (array_key_exists('next', $_GET)) {
$stateID = $_GET['next'];
}
if (empty($stateID)) {
if (!array_key_exists('AuthState', $_REQUEST) || empty($_REQUEST['AuthState'])) {
throw new SimpleSAML_Error_BadRequest('Missing state parameter on facebook linkback endpoint.');
}
$stateID = $_REQUEST['AuthState'];
$state = SimpleSAML_Auth_State::loadState($stateID, sspmod_authfacebook_Auth_Source_Facebook::STAGE_INIT);
/* Find authentication source. */
......@@ -30,11 +23,11 @@ if ($source === NULL) {
}
try {
if ($cancel) {
if (isset($_REQUEST['error_reason']) && $_REQUEST['error_reason'] == 'user_denied') {
throw new SimpleSAML_Error_UserAborted();
}
$source->authenticate($state);
$source->finalStep($state);
} catch (SimpleSAML_Error_Exception $e) {
SimpleSAML_Auth_State::throwException($state, $e);
} catch (Exception $e) {
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment