Created
March 21, 2015 04:19
-
-
Save oligriffiths/09b64cf58408b39562ee to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace Nooku\Component\Overrides; | |
use Nooku\Library; | |
use Nooku\Component\Users; | |
class UsersDispatcherAuthenticatorFacebook extends Library\DispatcherAuthenticatorAbstract | |
{ | |
/** | |
* A redirect url | |
* | |
* @var string | |
*/ | |
protected $_redirect_url; | |
/** | |
* The Facebook access token to be used | |
* | |
* @var string | |
*/ | |
protected $_access_token; | |
/** | |
* @var \DateTime | |
*/ | |
protected $_token_expiry; | |
/** | |
* Constructor. | |
* | |
* @param Library\ObjectConfig $config Configuration options | |
*/ | |
public function __construct(Library\ObjectConfig $config) | |
{ | |
parent::__construct($config); | |
$this->addCommandCallback('before.dispatch', 'authenticateRequest'); | |
$this->addCommandCallback('before.post', 'authenticateRequest'); | |
$this->_access_token = $config->access_token; | |
} | |
/** | |
* Initializes the options for the object | |
* | |
* Called from {@link __construct()} as a first step of object instantiation. | |
* | |
* @param ObjectConfig $config A ObjectConfig object with configuration options | |
* @return void | |
*/ | |
protected function _initialize(Library\ObjectConfig $config) | |
{ | |
$config->append(array( | |
'client_id' => '', | |
'client_secret' => '', | |
'redirect_url' => null, | |
'access_token' => null, | |
'scopes' => array(), | |
'auto_auth' => false, | |
'create_user' => true | |
)); | |
parent::_initialize($config); | |
} | |
/** | |
* Gets the facebook access token | |
* | |
* @return string | |
*/ | |
public function getAccessToken() | |
{ | |
return $this->_access_token; | |
} | |
/** | |
* Set the facebook access token | |
* | |
* @param $token | |
*/ | |
protected function setAccessToken($token) | |
{ | |
$this->_access_token = $token; | |
} | |
/** | |
* Gets the token expiry time | |
* | |
* @return \DateTime | |
*/ | |
public function getTokenExpiry() | |
{ | |
return $this->_token_expiry; | |
} | |
/** | |
* Set token expiry | |
* | |
* @param \DateTime $expiry | |
*/ | |
protected function setTokenExpiry(\DateTime $expiry) | |
{ | |
$this->_token_expiry = $expiry; | |
} | |
/** | |
* Authenticate user against facebook api | |
* | |
* @param Library\DispatcherContextInterface $context A dispatcher context object | |
* @return boolean Returns FALSE if the check failed. Otherwise TRUE. | |
*/ | |
public function authenticateRequest(Library\DispatcherContextInterface $context) | |
{ | |
//If authenticated, don't continue | |
if($context->getUser()->isAuthentic()){ | |
return; | |
} | |
$is_session = $context->getSubject()->getController()->getIdentifier()->package == 'users' && $context->getSubject()->getController()->getIdentifier()->name == 'session'; | |
$code = $context->getRequest()->getQuery()->code; | |
$access_token = $context->getRequest()->getQuery()->access_token ?: $this->getAccessToken(); | |
/** | |
* Execution only continues if no code or access token supplied, | |
* Action is dispatch and auto_auth is disabled, or action is post and it's not the user session controller | |
*/ | |
if(!$code && !$access_token && (($context->action == 'dispatch' && !$this->getConfig()->auto_auth) || ($context->action == 'post' && !$is_session))){ | |
return; | |
} | |
//If no code or access token, redirect to facebook to get code | |
if(!$access_token && !$code){ | |
$this->_redirectToFacebook($context); | |
return; | |
} | |
//Get access token from code | |
if($code){ | |
$this->exchangeCodeForAccessToken($code, $context); | |
//Validate the token | |
}else if($access_token){ | |
$this->validateAccessToken($access_token); | |
} | |
//If no access token, stop | |
if(!$this->getAccessToken()){ | |
return; | |
} | |
//Authenticate the user | |
if(false == $user = $this->_authenticateUser($this->getAccessToken(), $this->getTokenExpiry())){ | |
return; | |
} | |
//Set user data in context | |
$this->_loginUser($user->id, $context); | |
//Get the original querystring | |
$state = rawurldecode($context->getRequest()->getQuery()->state); | |
parse_str($state, $querystring); | |
//Start a session if fb_start_session is set, previously a POST based login | |
if($context->action == 'dispatch' && isset($querystring['fb_start_session']) && $querystring['fb_start_session'] == 1){ | |
$this->_startSession($context); | |
//Create new session | |
$this->getObject('com:users.controller.session')->add($context); | |
unset($querystring['fb_start_session']); | |
} | |
//If we were redirected with a code, redirect to the origin redirect URL with state | |
if($code){ | |
$redirect_url = $this->_getRedirectUrl($context); | |
$redirect_url->setQuery($querystring); | |
$context->response->setStatus(Library\DispatcherResponse::FOUND); | |
$context->response->setRedirect($redirect_url); | |
$this->send($context); | |
} | |
} | |
/** | |
* Starts the session | |
* | |
* @param Library\DispatcherContextInterface $context | |
*/ | |
protected function _startSession(Library\DispatcherContextInterface $context) | |
{ | |
$session = $context->getUser()->getSession(); | |
if(!$session->isActive()){ | |
//Set Session Name | |
$session->setName(md5($context->request->getBasePath())); | |
//Set Session Options | |
$session->setOptions(array( | |
'cookie_path' => (string) $context->request->getBaseUrl()->getPath() ?: '/', | |
'cookie_domain' => (string) $context->request->getBaseUrl()->getHost() | |
)); | |
//Start the session (if not started already) | |
$session->start(); | |
} | |
} | |
/** | |
* Gets the redirect URL for facebook | |
* | |
* @param Library\DispatcherContextInterface $context | |
* @return Library\HttpUrl | |
*/ | |
protected function _getRedirectUrl(Library\DispatcherContextInterface $context = null) | |
{ | |
if(!$this->_redirect_url){ | |
$redirect_url = $this->getConfig()->redirect_url; | |
//Convert url to relative | |
if($redirect_url && substr($redirect_url,0,4) != 'http'){ | |
$redirect_url = $context->getRequest()->getUrl()->toString(Library\HttpUrl::AUTHORITY).$redirect_url; | |
} | |
//Create URL object or use request URL | |
$redirect_url = $this->getObject('lib:http.url', array('url' => $redirect_url ?: $context->getRequest()->getUrl()->toString(Library\HttpUrl::AUTHORITY))); | |
//Facebook requires urls to end in a slash | |
$this->_redirect_url = $redirect_url->setPath(rtrim($redirect_url->getPath(),'/').'/'); | |
} | |
return $this->_redirect_url; | |
} | |
/** | |
* Redirect the user to the facebook oauth dialog/page | |
* | |
* @param Library\DispatcherContextInterface $context | |
*/ | |
protected function _redirectToFacebook(Library\DispatcherContextInterface $context) | |
{ | |
//Create scopes | |
$scopes = $this->getConfig()->scopes->toArray(); | |
if(!in_array('email', $scopes)){ | |
$scopes[] = 'email'; | |
} | |
$scopes = implode(',',$scopes); | |
$redirect_url = $this->_getRedirectUrl($context); | |
$querystring = $redirect_url->getQuery(true); | |
if($context->action == 'post' || $this->getConfig()->auto_auth){ | |
$querystring['fb_start_session'] = 1; | |
} | |
$querystring = rawurlencode(http_build_query($querystring)); | |
//Clear query string, facebook requires a path only url | |
$redirect_url->setQuery(array()); | |
//Redirect to facebook auth | |
$context->response->setStatus(Library\DispatcherResponse::FOUND); | |
$context->response->setRedirect('https://www.facebook.com/dialog/oauth?client_id='.$this->getConfig()->client_id.'&scope='.$scopes.'&redirect_uri='.$redirect_url.'&state='.$querystring); | |
$this->send($context); | |
} | |
/** | |
* Authenticates a user with facebook using the supplied access token. | |
* If user returned, a local user is created if create_user = true in config | |
* User account is updated with access token and expiry | |
* | |
* @param $access_token | |
* @param \DateTime $token_expiry | |
* @return bool | |
* @throws \RuntimeException | |
*/ | |
protected function _authenticateUser($access_token, \DateTime $token_expiry) | |
{ | |
//Fetch facebook user | |
$fb_user = $this->getFacebookUser($access_token); | |
//Ensure we have the users email address | |
if(!isset($fb_user->email) || !$fb_user->email){ | |
throw new \RuntimeException('User email address missing from facebook response'); | |
} | |
//Fetch user | |
$user = $this->getObject('com:users.model.users')->email($fb_user->email)->fetch(); | |
//Do nothing if the user is new and we're not creating users | |
if(!$this->getConfig()->create_user && $user->isNew()){ | |
return false; | |
} | |
//Format expiry date | |
$expiry_date = $token_expiry->format('Y-m-d H:i:s'); | |
//If new user, create user account | |
if($user->isNew()){ | |
$user = $this->_createUser(array( | |
'email' => $fb_user->email, | |
'name' => $fb_user->name, | |
'facebook_access_token' => $access_token, | |
'facebook_token_expiry' => $expiry_date | |
)); | |
} | |
//Update the token and expiry if different | |
if($user->facebook_access_token != $access_token || $user->facebook_token_expiry != $expiry_date){ | |
$user->facebook_access_token = $access_token; | |
$user->facebook_token_expiry = $expiry_date; | |
$user->save(); | |
} | |
return $user; | |
} | |
/** | |
* Creates a new user with the supplied data, disables activation as we know the user is active | |
* | |
* @param array $user_data | |
* @return mixed | |
*/ | |
protected function _createUser($user_data = array()) | |
{ | |
$user_data['password'] = hash_hmac('sha256', time().$user_data['facebook_access_token'], $this->getConfig()->client_secret); | |
$user = $this->getObject('com:users.controller.user')->add($user_data); | |
$user->password = null; | |
$user->enabled = 1; | |
$user->activation = null; | |
$user->save(); | |
return $user; | |
} | |
/** | |
* Log the user in | |
* | |
* @param string $username A user key or name | |
* @param array $data Optional user data | |
* | |
* @return bool | |
*/ | |
protected function _loginUser($username, Library\DispatcherContextInterface $context) | |
{ | |
//Set user data in context | |
$data = $this->getObject('user.provider')->load($username)->toArray(); | |
$data['authentic'] = true; | |
$context->getUser()->setData($data); | |
} | |
/*** | |
* FACEBOOK API access methods | |
*/ | |
/** | |
* Produces the app token string | |
* | |
* @return string | |
*/ | |
public function getAppToken() | |
{ | |
return $this->getConfig()->client_id.'|'.$this->getConfig()->client_secret; | |
} | |
/** | |
* Gets the access token from facebook by exchanging the code for an access token | |
* | |
* @param $code | |
* @return Library\ObjectConfig | |
*/ | |
public function exchangeCodeForAccessToken($code, Library\DispatcherContextInterface $context) | |
{ | |
$client_id = $this->getConfig()->client_id; | |
$client_secret = $this->getConfig()->client_secret; | |
$redirect_url = $this->_getRedirectUrl($context); | |
$redirect_url->setQuery(array()); | |
//Make request for token | |
$response = $this->_queryFacebook('oauth/access_token', array('client_id' => $client_id, 'client_secret' => $client_secret, 'redirect_uri' => (string) $redirect_url, 'code' => $code)); | |
//Parse out the response, it's a querystring | |
$query = array(); | |
parse_str($response, $query); | |
$token = new Library\ObjectConfig($query); | |
//Store access token | |
$access_token = $token->access_token; | |
$this->setAccessToken($access_token); | |
//Convert expiry date | |
$expiry_date = new \DateTime(); | |
$expiry_date->add(new \DateInterval('PT'.$token->expires.'S')); | |
$this->setTokenExpiry($expiry_date); | |
return $token; | |
} | |
/** | |
* Validate an access token | |
* | |
* @param $token | |
* @return bool | |
*/ | |
public function isAccessTokenValid($token) | |
{ | |
$response = $this->validateAccessToken($token); | |
return isset($response->data) && isset($response->data->is_valid) && $response->data->is_valid; | |
} | |
/** | |
* Validates an access token against the facebook api, returning the token information or error | |
* | |
* @param $token | |
* @return mixed | |
*/ | |
public function validateAccessToken($access_token) | |
{ | |
$token = $this->_queryFacebook('debug_token', array('input_token' => $access_token, 'access_token' => $this->getAppToken())); | |
//Ensure correct token data present | |
if(!$token || !(isset($token->data) && isset($token->data->is_valid) && $token->data->is_valid)){ | |
throw new \RuntimeException('The supplied access token is invalid'); | |
} | |
//Store token | |
$this->setAccessToken($access_token); | |
//Store expiry date | |
$expiry_date = new \DateTime(); | |
$expiry_date->add(new \DateInterval('PT'.$token->data->expires_at.'S')); | |
$this->setTokenExpiry($expiry_date); | |
return $token; | |
} | |
/** | |
* Gets the user object from facebook | |
* | |
* @param $token | |
* @return object | |
*/ | |
public function getFacebookUser($token) | |
{ | |
return $this->_queryFacebook('me', array('access_token' => $token)); | |
} | |
/** | |
* Queries facebook API for the url and the given query params | |
* | |
* @param $url | |
* @param array $params | |
* @return mixed | |
* @throws \RuntimeException | |
*/ | |
protected function _queryFacebook($url, $params = array()) | |
{ | |
$curl = curl_init(); | |
curl_setopt_array($curl, array( | |
CURLOPT_RETURNTRANSFER => 1, | |
CURLOPT_URL => 'https://graph.facebook.com/'.$url.'?'.http_build_query($params) | |
)); | |
//Fetch curl result | |
$response = curl_exec($curl); | |
//Ensure the result was successful | |
$httpcode = curl_getinfo($curl, CURLINFO_HTTP_CODE); | |
if($httpcode != 200){ | |
$data = json_decode($response); | |
throw new \RuntimeException(isset($data->error) ? $data->error->message : 'An unexpected error occurred whilst communicating with facebook. '.$response); | |
} | |
//Detect content type | |
$content_type = array_shift(explode(';',curl_getinfo($curl, CURLINFO_CONTENT_TYPE))); | |
//Return json decoded object if json | |
return $content_type == 'application/json' ? json_decode($response) : $response; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment