Skip to content

Instantly share code, notes, and snippets.

@oligriffiths
Created March 21, 2015 04:19
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save oligriffiths/09b64cf58408b39562ee to your computer and use it in GitHub Desktop.
Save oligriffiths/09b64cf58408b39562ee to your computer and use it in GitHub Desktop.
<?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