home > client-side, server-side > Integrating Facebook Connect with CakePHP’s Auth component

Integrating Facebook Connect with CakePHP’s Auth component

I wanted to be able to leverage all of the advantages of using Cake’s built in Auth component in my latest application; problem was that the application needed to allow for both normal user accounts and Facebook Connect generated user accounts. I struggled for a while to find the most seamless approach, and then it clicked — dynamically set Auth->fields.

First, drop the facebook client libraries in a app/vendors/facebook folder. Then, in your app_controller, import the classes, and set some properties:

<?php
App::import('Vendor', 'facebook/facebook');

class AppController extends Controller {
    var $components = array('Auth');
    var $uses = array('User');
    var $facebook;
    var $__fbApiKey = 'your_key';
    var $__fbSecret = 'your_secret';
}

Next we’ll overwrite the inherited __construct method to instantiate a Facebook Object:

	function __construct() {
		parent::__construct();

		// Prevent the 'Undefined index: facebook_config' notice from being thrown.
		$GLOBALS['facebook_config']['debug'] = NULL;

		// Create a Facebook client API object.
		$this->facebook = new Facebook($this->__fbApiKey, $this->__fbSecret);
	}

In our beforeFilter method, we define the default Auth properties, call a private __checkFBStatus method, and pass any user data to the view:

function beforeFilter() {
    // Authentication settings
    $this->Auth->fields = array('username' => 'email', 'password' => 'password');
    $this->Auth->logoutRedirect = '/';

    //check to see if user is signed in with facebook
    $this->__checkFBStatus();

    //send all user info to the view
    $this->set('user', $this->Auth->user());
}

Next we’ll define the private method __checkFBStatus that’s called in our beforeFilter method. First we check to make sure $this->Auth->User isn’t already set by a normal user account, and we check to see if there’s a logged in facebook user. The logged in facebook user is available after we use the JavaScript API to log facebook users into the site.

    private function __checkFBStatus(){
        //check to see if a user is not logged in, but a facebook user_id is set
        if(!$this->Auth->User() && $this->facebook->get_loggedin_user()):

Next, see if this user has already logged in, and therefore already has an entry in our User table:

            //see if this facebook id is in the User database; if not, create the user using their fbid hashed as their password
            $user_record =
                $this->User->find('first', array(
                    'conditions' => array('fbid' => $this->facebook->get_loggedin_user()),
                    'fields' => array('User.fbid', 'User.fbpassword', 'User.password'),
                    'contain' => array()
                ));

If no record was found, we create a new record for this user. We are setting 3 variables, fbid, fbpassword, and password. The fbpassword field will hold a randomly generated 20 character string un-hashed so that we can access the value of the field directly from the database. We retrieve this value based on the fbid field, hash it, and that’s our password as the Auth Component expects:

            //create new user
            if(empty($user_record)):
                $user_record['fbid'] = $this->facebook->get_loggedin_user();
                $user_record['fbpassword'] = $this->__randomString();
                $user_record['password'] = $this->Auth->password($user_record['fbpassword']);

                $this->User->create();
                $this->User->save($user_record);
            endif;

We need to then update our Auth Component’s fields property to use fbid as the username.

            //change the Auth fields
            $this->Auth->fields = array('username' => 'fbid', 'password' => 'password');

            //log in the user with facebook credentials
            $this->Auth->login($user_record);

        endif;
    }

Here’s the complete app_controller.php:

<?php
App::import('Vendor', 'facebook/facebook');

class AppController extends Controller {
    var $components = array('Auth');
    var $uses = array('User');
	var $facebook;
	var $__fbApiKey = 'your_key';
	var $__fbSecret = 'your_secret';

	function __construct() {
		parent::__construct();

		// Prevent the 'Undefined index: facebook_config' notice from being thrown.
		$GLOBALS['facebook_config']['debug'] = NULL;

		// Create a Facebook client API object.
		$this->facebook = new Facebook($this->__fbApiKey, $this->__fbSecret);
	}

    function beforeFilter() {
		// Authentication settings
		$this->Auth->fields = array('username' => 'email', 'password' => 'password');
		$this->Auth->logoutRedirect = '/';

        //check to see if user is signed in with facebook
        $this->__checkFBStatus();

        //send all user info to the view
        $this->set('user', $this->Auth->user());
	}

    private function __checkFBStatus(){
        //check to see if a user is not logged in, but a facebook user_id is set
        if(!$this->Auth->User() && $this->facebook->get_loggedin_user()):

            //see if this facebook id is in the User database; if not, create the user using their fbid hashed as their password
            $user_record =
                $this->User->find('first', array(
                    'conditions' => array('fbid' => $this->facebook->get_loggedin_user()),
                    'fields' => array('User.fbid', 'User.fbpassword', 'User.password'),
                    'contain' => array()
                ));

            //create new user
            if(empty($user_record)):
                $user_record['fbid'] = $this->facebook->get_loggedin_user();
                $user_record['fbpassword'] = $this->__randomString();
                $user_record['password'] = $this->Auth->password($user_record['fbpassword']);

                $this->User->create();
                $this->User->save($user_record);
            endif;

            //change the Auth fields
            $this->Auth->fields = array('username' => 'fbid', 'password' => 'password');

            //log in the user with facebook credentials
            $this->Auth->login($user_record);

        endif;
    }

    private function __randomString($minlength = 20, $maxlength = 20, $useupper = true, $usespecial = false, $usenumbers = true){
        $charset = "abcdefghijklmnopqrstuvwxyz";
        if ($useupper) $charset .= "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
        if ($usenumbers) $charset .= "0123456789";
        if ($usespecial) $charset .= "~@#$%^*()_+-={}|][";
        if ($minlength > $maxlength) $length = mt_rand ($maxlength, $minlength);
        else $length = mt_rand ($minlength, $maxlength);
        $key = '';
        for ($i=0; $i<$length; $i++){
            $key .= $charset[(mt_rand(0,(strlen($charset)-1)))];
        }
        return $key;
    }

}
?>

Next we need to make a few changes to our default.ctp layout; first add the facebook namespace:

<html xmlns="http://www.w3.org/1999/xhtml" xmlns:fb="http://www.facebook.com/2008/fbml">

Add these 2 javascript snippets (be sure your xd_receiver.htm file is accessible in your webroot):

<script type="text/javascript" src="http://static.ak.connect.facebook.com/js/api_lib/v0.4/FeatureLoader.js.php"></script>
<script type="text/javascript">FB.init("your_api_key","/xd_receiver.htm");</script>

Last step, place your login and logout buttons in your default view. Few things to point out here; onlogin="window.location.reload();" will cause the page to reload after a successful Facebook Connect login. This is how we are able to access $this->facebook->get_loggedin_user() in our app_controller to force the manual Auth login. If the user is a facebook user (determined with $user['User']['fbid'] > 0), add an onclick event that calls the FB.Connect.logout() function with a redirect URL as the parameter. This will log the User out of the Auth Component after it logs the user out of Facebook.

<?php
            if(!empty($user)):
                if($user['User']['fbid'] > 0):
                    echo $html->link('logout', '#', array('onclick' => 'FB.Connect.logout(function() { document.location = \'http://your_server/users/logout/\'; }); return false;'));
                else:
                    echo $html->link('logout', array('controller' => 'users', 'action' => 'logout'));
                endif;
            else:
                echo '<fb:login-button onlogin="window.location.reload();"></fb:login-button>';
            endif;
?>

One last thing to consider, for security reasons… In your login method, be sure only users with fbid = 0 can login via the normal auth fields (username / password). Just an extra precaution considering you have an unhashed password in your database for those facebook users.

Hope that helps someone else out. Let me know if you run into any problems, I slapped this post up quickly and hopefully didn’t make many mistakes. And per Matt’s spot-on recommendation (via pseudocoder.com), I’m working on rolling this into a plugin… Will post when it’s complete.

kettle client-side, server-side , , ,

  1. June 2nd, 2009 at 12:46 | #1

    Awesome implementation! It would really be great if this was a plug-in. If I had the time, I might have even taken it further and make a more generic plugin to allow Facebook/Google/OpenID authentication.

    This will make for a good starting point, though. :)

  2. June 2nd, 2009 at 22:41 | #2

    @Ben Pesso
    Thanks Ben, glad it helped get you started. Would love to see your plug-in once you’ve completed it. This was really meant more as a proof of concept, I thought perhaps it might make more sense to develop as a component, which is the plan as soon as I get some time…

  3. ETW
    July 19th, 2009 at 16:42 | #3

    When I call get_loggedin_user() it is always null. I’m logging in the user with FB Connect JS, but cookies are cookies, right? Doesn’t Cake hijack all the Session information?

  4. gaurav sharma
    July 29th, 2009 at 01:29 | #4

    very nice and informative tutorial. I was looking for one and finally found it…. thanks a lot… :-)

  5. August 1st, 2009 at 03:24 | #5

    line 21 in app_controller must be beforeRender, otherwise it never worked for me …

  6. August 1st, 2009 at 11:08 | #6

    @dimatter
    That’s strange; per most documentation I’ve read, those Auth variables are typically set in beforeFilter. Glad you figured out a solution that worked for you, but I wonder if there’s something else in your code that might be requiring you to use beforeRender…

  7. Ben
    August 25th, 2009 at 20:47 | #7

    Thanks Kettle for a great tutorial - really clarifies how to use FB Connect. Looking forward to seeing it in plugin form - are you going to SVN it on bakery or something?

    Thanks again.

  8. Tom
    September 23rd, 2009 at 15:33 | #8

    I also had to move some things to the beforeRender() in order for it to work (CakePHP 1.2.4.8284).
    Just the check for FB status…
    function beforeRender() {
    $this->__checkFBStatus();
    //send all user info to the view
    $this->set(’user’, $this->Auth->user()); // we’ll do this later
    }

  9. Tom
    September 23rd, 2009 at 18:24 | #9

    Actually…I’m also getting confused now after getting more familiar with all this…Why a password is being generated and stored unhashed? Why not just generate a uid and pass it through Cake’s hash and not store the unhashed uid in field fbpassword?

    I personally also set their username to be fb#### that way you don’t need to make the auth variable use email field as the login. So for security…One would not only need to know your prefixed username and the user’s facebook id then, but also would need to somehow know the uid to login. I’ll drop a dart from outerspace and hit my house first.

    The sad thing about FB Connect is it’s practically useless for most sites. We want more info from our users most of the time…Things like e-mail, first name, last name…in the least if not addresses. You can’t get that from FB connect and then there’s privacy policies all over FB. So you sadly need to prompt a user to “complete their registration” (either immediately or later). So at that point they can perhaps choose a username and set their password. Leaving the only necessary field here the fbid so that we can hook in here like you have done and see if FB API gives us a facebook user id that we have in our database. If so. Let them in. Afterall, we’re trusting that Facebook has authenticated the user and their account hasn’t been hacked…and FB connect is free of session hijacking, etc.

    Or, don’t ever prompt a user to “register” I mean that is entirely up to each CakePHP app. I don’t want to get off topic…But am I missing something? What’s the random string function for? Why store an unhashed pw? Why even bother w/ the password field? Other than to make it something that can’t be guessed? So can all this really be slimmed down? What am I missing?

  10. September 23rd, 2009 at 19:07 | #10

    @Tom
    Hey Tom:

    It’s been a long while since I wrote this, so sorry if I’m a little fuzzy on some details — but as I mentioned, it’s just a POC and certainly needs refactoring. I would like to come back to this if I could find the time…

    If I remember correctly, I think the answer to the majority of your questions is that this particular approach satisfied some specific needs the rest of the application had. For example, I’m using email as the login method for normal site registrants / visitors. Nothing to do with FB Connect here, just a product decision, so no username in my users table… And because I possibly could later store a FB Connect user’s email address in that field, I couldn’t use that field to store the hashed userId.

    I agree FB Connect has some serious limitations, but for this case, that additional registration info wasn’t necessary. Also, don’t recall seeing anything in the privacy policy that prevents you from using FB API data to prepopulate form fields…

  11. Tom
    September 24th, 2009 at 12:57 | #11

    cool. totally understand and agree.
    what can you get from the fb api? they just give a user id, right? without their extended permissions which is a setting a normal user probably won’t bother setting. i know that’s a question i need to take up with their docs, but for the life of me i can’t find anything there…things changed…and then i read what you are allowed to store in your db and what you aren’t.

    i really think the main purpose of fb connect is a marketing gimmick for facebook than it is a promotion technique for one’s site. the benefits are stacked far into fb’s favour and it just gets developers onboard to promote facebook… i’m left to see the benefits (other than posting some info from your site on to a user’s wall and maybe invite their friends…of course to start with only up to 3 which is lame). Again gimmicks. Sadly, because FB is so popular, I have to play into the gimmick.

    it really would have been as simple as fb allowing for a checkbox saying “yes i agree to migrate my profile data to XXXX.com” …

  12. September 24th, 2009 at 13:52 | #12

    Oh you can temporarily retrieve tons of data; FB’s policy requires you don’t keep anything that’s not labeled ’storable’ for no more than 24 hours.

    Friends, videos and photos by tag, status updates, likes, comments, all kinds of stuff… I’ve found FB Connect most useful for simple content syndication and user registration (no user wants to keep track of all 500 logins they use for various sites). A user performs an action on my site, like leaving a comment. That comment can be saved in my DB, it’s not proprietary FB data, and then I can use the API to publish the user’s activity, namely leaving the comment, on FB. Then all of their friends know about that user’s activity on my site, and they visit (hopefully).

    Take a look at the API documentation: http://wiki.developers.facebook.com/index.php/API

    All of those methods can be called very easily once you have a valid session. Some of them don’t even require a valid session. And you’re right, some require special permissions, like RSVP’ing to an event, or uploading photos on a user’s behalf. But the marketing opportunities are endless.

  13. Tom
    September 24th, 2009 at 16:13 | #13

    @kettle
    Thanks! This really has been an extremely helpful blog post.

  14. September 24th, 2009 at 16:23 | #14

    @Tom
    Glad it finally won you over!

  15. Tom
    September 24th, 2009 at 16:41 | #15

    @Tom
    aha…and since you can’t store data for more than 24hrs (over that is considered “indefinitely”)…I’ll just run an afterFind() and populate data from the API.. View caching set to 24hrs…and voila. It’s as good to me as if it was stored in my own db. I can’t afford to not use the caching though…How can I tell visitors to sit there and wait all that time while the API fetches data? I won’t let FB tell me how to poorly run my site. I’ll just follow their policy to it’s very limits =) Food for thought if you roll things up into a plugin, which I’d be all too anxious to see. Thanks again.

  16. October 24th, 2009 at 19:02 | #16

    Hi and firstly thanks for this tutorial.

    Earlier today I integrated Sign in with Twitter based around this concept and I am now trying to get Facebook Connect working too.

    I am fairly happy with how far I have got other than the fact that my login seems to be working fine, but the user experience is poor.

    This is because when the user is already logged into facebook the page *instantly* logs the user in but does not reload/redirect the page so you can’t tell if anything has happened till you load another page.

    When the user is not logged into facebook you get the facebook login box which closes once you supply valid login details, but again the main window does not redirect/reload so you’re left with the main login page.

    Any ideas how I get the auth->redirect() to work as it normally would, once that’s working I would be sorted!

    I have placed onlogin=”window.location.reload();” in body which I suppose must be doing it’s job as the facebook login process is able to create a user based on the facebook data, but I need the page to reload.

  17. October 24th, 2009 at 19:25 | #17

    For reference, you can check my integration in action at http://www.localcause.org.uk, I don;t have the login/logout links in my layout, I only have the login button in my login view.

  18. November 7th, 2009 at 03:58 | #18

    Thought I’d post a follow-up to the above comments as I am getting a fair bit of traffic from them. Unfortunately I did not get a reply from the author of this blog, and as our site is frantically being developed to get it ready for a full-launch I had to pull the Facebook Connect integration for now.

    I plan to have another go at this over the weekend though as I now have Sign in with Twitter working perfectly with CakePHP so I’m more confident that I will be able to resolve my Facebook Connect issues too, with a little help from their support team I hope.

    I will post an update when I have it.

    Paul.

  19. November 30th, 2009 at 18:08 | #19

    Check the api and secret keys for those who are getting NULL in get_loggedin_user

    I played few days and then saw what I am using keys from other applications.

    Enable debug and you will see the facebook div with info if connected succesfully

  1. May 28th, 2009 at 12:07 | #1
  2. June 1st, 2009 at 22:15 | #2
  3. June 7th, 2009 at 15:08 | #3
  4. July 23rd, 2009 at 12:59 | #4
  5. August 23rd, 2009 at 17:44 | #5