This is a post for anyone interested in implementing Facebook Connect on a Symfony 1.x project. There are of course many ways to do this…but here are some of the driving factors behind the approach we describe below:
- We wanted to leverage Facebook to handle authentication, provide a seamless sign up experience, and allow us to use social features
- We wanted some control over the session – meaning once a user was authenticated we wanted to operate independently of whether or not she remained logged into Facebook
- Along those same lines, we wanted to set our own “remember me” cookie to identify returning users as opposed to relying on the Facebook session
- We wanted to be able to drop users back on the page from which they logged in, which meant refreshing the page upon login to update user-specific content
- We wanted to grab fresh authentication tokens whenever possible, but do not use them very much at the moment. Since we’ve allowed the scenario of someone being logged into our site without an active Facebook session, the case could arise that we have a logged in user but no access to her current Facebook data. The simple solution is to ask the user to log into Facebook at the point when that information is needed.
- And lastly, we are using the sfGuard plugin for handling users
So let’s get down to it…here’s what we did:
1) Load the Facebook Javascript SDK in the layout, just after the body tag. This makes sure it gets loaded on every page at the right time.
//in apps/frontend/templates/layout.php
<body>
<!-- START FACEBOOK JAVASCRIPT SDK -->
<div id="fb-root"></div>
<script>
window.fbAsyncInit = function() {
FB.init({
appId : <?php echo sfConfig::get('app_facebook_app_id') ?>, // stored in app.yml for convenience
//channelUrl : '//WWW.YOUR_DOMAIN.COM/channel.html', // Channel File (we haven't implemented this yet, so we just comment it out)
status : false, // check login status (we don't make use of this)
cookie : true, // enable cookies to allow the server to access the session
xfbml : true // parse XFBML
});
};
// Load the SDK Asynchronously
(function(d){
var js, id = 'facebook-jssdk', ref = d.getElementsByTagName('script')[0];
if (d.getElementById(id)) {return;}
js = d.createElement('script'); js.id = id; js.async = true;
js.src = "//connect.facebook.net/en_US/all.js";
ref.parentNode.insertBefore(js, ref);
}(document));
</script>
<!-- END FACEBOOK JAVASCRIPT SDK -->
<div id="main">
// Page content
2) Attach Facebook’s login method to our custom sign up/log in button. In the callback function, if the user is successfully authenticated, make an AJAX call to an action that will handle the user data, then reload the page. To ensure the action gets completed before the page reload, we made the AJAX call synchronous. (UPDATE: Thought of a better way to handle the page reload…simply execute in the ‘complete’ callback function of the AJAX call) This approach was driven by the fact that we wanted to drop users back onto the page that they were already on. If you have a destination page where users are sent upon login, then this doesn’t need to be done in AJAX…the code for handling the Facebook data could be put into the action for that landing page.
//in web/js/scripts.js
//Feel free to attach this function to your login button however you'd like
//Function gets executed when the login button in the header is clicked
function onClickloginfb() {
//Use the FB object's login method from the Facebook Javascript SDK to authenticate the user
//If the user has already approved your app, she is simply logged in
//If not, the app authentication dialog box is shown
FB.login(function(response) {
//If the user is succesfully authenticated, we execute some code to handle the freshly
//logged in user, if not, we do nothing
if (response.authResponse) {
//Use ajax to execute an action that handles authenticated user
$.ajax({
url: "/facebook-connect-login",
complete: function(){
//Reload the page after the user is authenticated to update user-specific elements
window.location.reload();
}
});
}
}, {scope:'email'}); //we just ask for the email permission
}
3) The new action that handles the Facebook data makes use of Facebook’s PHP SDK…we use some custom code that makes a call to Facebook’s graph API to grab the user’s info…but this could be done via functions in the SDK as well. If the user is not in our database, then a new user is created and populated with data (we created a function in the sfGuardUser model class to handle this). If the user IS in the database already, we simply grab a fresh profile picture, and grab a fresh Access Token from Facebook.
//in apps/frontend/modules/user/actions/actions.class.php
//The action executed via ajax after the facebook authentication (/facebook-connect-login)
//This is where the real magic happens
public function executeFacebookConnectLogin(sfWebRequest $request)
{
//Creating a new Facebook object from the Facebook PHP SDK
require_once sfConfig::get('sf_lib_dir').'/vendor/facebook-php-sdk/src/facebook.php';
$facebook = new Facebook(array(
'appId' => sfConfig::get('app_facebook_app_id'),
'secret' => sfConfig::get('app_facebook_secret'),
));
//Get user object from Facebook - this method is only executed after we've confirmed that the user
//has an active session with facebook and that our app is approved...so it should work
$facebook_user = $facebook->getUser();
if ($facebook_user)
{
try
{
// Proceed knowing you have a logged in user who's authenticated through facebook
$access_token = $facebook->getAccessToken();
//Grab the user's data from the facebook graph API using the fresh access token
//we define which fields we want in app.yml ("id,first_name,last_name,picture,email,gender,friends")
$requestUrl = 'https://graph.facebook.com/me?access_token='
.$access_token
.'&fields='.sfConfig::get('app_facebook_fields');
$response = @file_get_contents($requestUrl);
//TODO: Down the road, try new method of getting info here:
//http://developers.facebook.com/docs/reference/php/facebook-api/
//$facebook_user_profile = $facebook->api('/me');
if($response)
{
$facebook_user_profile = json_decode($response);
if(!is_null($facebook_user_profile->email)) //just to make sure we have the right data
{
//Check to see if user is already in our database
$user = Doctrine::getTable('sfGuardUser')->getUserByFacebookId($facebook_user_profile->id);
//If no, create a new user instance and populate with data from facebook
if(!$user)
{
$user = new sfGuardUser();
//This function saves the user's first name, last name, gender, email address,
//picture, and Facebook id
$user->getUserDataFromFacebook($facebook_user_profile);
//Save the fresh access token to the DB for later use
$user->setFbAccessToken($access_token);
//Save new user data to database
$user->save();
}else
{
//If user IS already in the database, just refresh the profile picture
//and access token.
$user->setProfilePicture($facebook_user_profile->picture);
$user->setFbAccessToken($access_token);
$user->save();
}
//Finally, log the user in using sfGuard method
if($user)
{
if (!$this->getUser()->isAuthenticated())
{
//Second parameter activates the remember me cookie when set to true
$this->getUser()->signIn($user, true);
}
}
}
}
}
catch (FacebookApiException $e)
{
error_log($e);
$facebook_user = null;
}
}
//This is all done via AJAX, so no html is rendered
return sfView::NONE;
}
4) The method we add to the sfGuardUser model for saving the data from Facebook is very straightforward.
//in lib/model/doctrine/sfGuardUser.class.php
//the sfGuardUser method to save the data from facebook
public function getUserDataFromFacebook($facebook_user_profile)
{
$this->setFirstName($facebook_user_profile->first_name);
$this->setLastName($facebook_user_profile->last_name);
$this->setEmailAddress($facebook_user_profile->email);
//These last three fields are actually in the sfGuardUserProfile table, but we made proxy
//methods in the sfGuardUser object for convenience
$this->setFacebookId($facebook_user_profile->id);
$this->setGender($facebook_user_profile->gender);
$this->setProfilePicture($facebook_user_profile->picture);
}
Some Closing Thoughts:
As of right now, we are not doing anything else outside of authentication and the initial grab of user data that makes use of the Access Token. We store the freshest one in the database and at some point we may use it to do things like hit the graph API to record when users take certain actions that we want to display on their timelines.
Once the user is authenticated through Facebook, we simply use sfGuard’s standard signin function with the rememberme option set to TRUE. This sets the rememberme cookie in their browser (defaults to 15 day lifetime), and we have activated the rememberme filter in the frontend app to check for this when a user first hits the site. This means that once the user is signed in, we are completely independent from her Facebook session (i.e. she could log out of Facebook in another tab and still be logged into our website)…when the time comes that we start to use the Access Token this will (rarely) present an issue, but we will simply ask users to sign in again if they want to perform an action that requires us to have a valid Access Token.
Thoughts? Suggestions? Feedback? Let us know what you think in the comments – we’d love to hear how this works for others and if there are ways to improve it!