Wednesday, February 29, 2012

How to integrate Facebook login with your website to authenticate users


Say you have a website and you would like to authenticate people before they can do some actions. Now, many people may not be very willing to fill out your registration form and do a login. Now if you allow people to use their existing Facebook or twitter Login for authentication then you are removing a big barrier from trying out your site.


This post  deals with server side integration of Facebook Login into your website (or online app!)  I am going to present it in a very sequential fashion.


We only cover the server side integration. If you are looking for javascript integration then stop reading now :) I believe server integration is a better scheme as it gives you better control. So if you are running your own website and would like to authenticate users with Facebook login then dive straight in.


Register with Facebook developers to get App ID and App Secret Key

First head over to  https://developers.facebook.com/apps  and register your application. Click on create new app button if you do not have any. The registration is straightforward.


  • App display name - chose carefully because this is the application name presented to people who are trying to access your site using their Facebook login.
  • App domain - Here you can put multiple domains + sub domains. Sub domains are especially useful if you plan to test on your local machine. Like in my case I have created two entries
    • mint.3mik.com (to test from my local linux mint VM)
    • www.3mik.com
  • Site URL - put your website url here - like http://www.3mik.com
  • Mobile web - like http://m.3mik.com
  • You do not have to register App on Facebook, Native iOs app and native android app if you do not want to. I just wanted the website integration and I checked off the other options.
  • After a successful registration, note down your App ID and App secret key

Create a  link on your website  to launch Facebook oAuth Dialog box

When users come to your site you have to present a link to launch Facebook oAuth dialog box. You can put a simple link or wrap it up in a nice book. When users come to your site, they see a Login with Facebook link.

The best resource for Facebook login server side integration   is  

To create such a link that will launch Facebook authentication dialog box, You need
  1. Your Facebook App ID 
  2. Your callback URL (where Facebook will redirect after a successful login) - Do not worry too much about this right now. Just add a URL that belongs to your site.
Here is how I am creating it


<?php
    $stoken = Util::getMD5GUID();
    $gWeb->store("fb_state",$stoken);
    $fbAppId = Config::getInstance()->get_value("facebook.app.id");
    $host = "http://".$_SERVER["HTTP_HOST"];
    $fbCallback = $host."/callback/fb2.php" ;
    
    $fbDialogUrl = "https://www.facebook.com/dialog/oauth?client_id=".$fbAppId ;
    $fbDialogUrl .= "&redirect_uri=".urlencode($fbCallback)."&scope=email&state=".$stoken ;
?>

  <a href="<?php echo $fbDialogUrl; ?>"> Login with Facebook</a>

some explanation is in order for parameters supplied to Facebook dialog URL

  • fbAppId - is simply the Facebook application ID
  • redirect_uri is where Facebook should redirect the user (A callback for us) 
  • scope - if you just want the basic information back then you do not have to use this parameter. in Our case we want Facebook to return email together with basic information hence we pass this extra parameter, scope=email. You can pass in a comma separated list.
  • state token. This is to prevent CSRF attacks. We generate a random token and keep it in our server session. We pass the same token as state parameter to Facebook. Facebook will return this state token as it is to our callback URL. There we can retrieve this token and compare it with the token stored in our session.

Now when users come to your login page, they will see a login with Facebook link. When they click it they will go to Facebook oAuth dialog page. After their authentication they will be redirected to our callback page.


Callback Page to process Facebook response

Most of the example code I have seen on parsing Facebook response is pathetic including Facebook's own sample. They are suppressing errors in their sample code. Real world applications will need a robust error handling and user information processing. 

Error Handler 

We want to trap any errors, log it and then redirect the users to login page where they can view the error. Also we log the errors so we can see why our integration is not working.  Here is our Error Handler



function login_error_handler($errorno,$errorstr,$file,$line) {

    if(error_reporting() == 0 ) {
        // do nothing for silenced errors
        return true ;
    }
    
    switch($errorno) {

        case E_STRICT :
            return true;
        case E_NOTICE :
        case E_USER_NOTICE :
            Logger->error(" $file :: $line :: $errorstr");
            break ;

        case E_USER_ERROR:
            Logger->trace($file,$line,$errorstr,'TRACE');
            $_SESSION["form.errors"] = array($errorstr);
            header('Location: /user/login.php');
            exit(1);
   

        default:
            Logger->trace($file,$line,$errorstr,'TRACE');
            $_SESSION["form.errors"] = array("Error happened during login");
            header('Location: /user/login.php');
            exit(1);
            
    }
    
    //do not execute PHP error handler
    return true ;
}





We use the above error handler in our callback code. Now coming to callback, following is our scheme to process Facebook response


  • Facebook can return error and error descriptions to callback. If Facebook returned an error then we have to process those errors and show them to users. I suggest reading the Facebook document here.
  • If there is no code then user has accessed the callback page directly and we need to launch the Dialog box
  • If you have a code and state returned by Facebook is same as stored by us in session (to prevent CSRF attacks) then we need to 
    • Issue a request to Facebook using the returned code and our App secret key 
    • Parse the data returned by Facebook
    • Use this data to start our own login session

The code in isolation will not make much sense. Here is the code to do error checks and issue a request to Facebook to get user data using our App secret key.





 
    //set special error handler for callback scripts 
    include ($_SERVER['APP_WEB_DIR'].'/callback/error.inc');
    set_error_handler('login_error_handler');
   
    $fbAppId = Config::getInstance()->get_value("facebook.app.id");
    $fbAppSecret = Config::getInstance()->get_value("facebook.app.secret");

    $host = "http://".$_SERVER["HTTP_HOST"];
    $fbCallback = $host. "/callback/fb2.php";
  
    $code = NULL;
    if(array_key_exists('code',$_REQUEST)) {
        $code = $_REQUEST["code"];
    }
 
    $error = NULL ;
    if(array_key_exists('error',$_REQUEST)) {
       $error = $_REQUEST['error'] ;
       $description = $_REQUEST['error_description'] ;
       $message = sprintf(" Facebook returned error :: %s :: %s ",$error,$description);
       trigger_error($message,E_USER_ERROR);

       exit ;
     }


     if(empty($code) && empty($error)) {
        //see how to launch an FB dialog again on Facebook URL given above
     }

    //last state token
    $stoken = $gWeb->find('fb_state',true);
 
    if(!empty($code) && ($_REQUEST['state'] == $stoken)) {
    
    //request to get access token
    $fbTokenUrl = "https://graph.facebook.com/oauth/access_token?client_id=".$fbAppId ;
    $fbTokenUrl .= "&redirect_uri=" . urlencode($fbCallback). "&client_secret=" . $fbAppSecret ;
    $fbTokenUrl .= "&code=" . $code;
  
    $response = file_get_contents($fbTokenUrl);
    $params = null;
    parse_str($response, $params);

    $graph_url = "https://graph.facebook.com/me?access_token=".$params['access_token'];
    $user = json_decode(file_get_contents($graph_url));
    processUser($user);

   }
   else {
    $message = "3mik.com and Facebook statedo not match.possible CSRF.";
    trigger_error($message,E_USER_ERROR);
  }




How to process the User information

Now you have the user information. At this point you can just dump the $user variable to see what all you get back from Facebook. From here on it depends on what you want to do. A simple scheme could be to


  • Look at the $user->id  
  • see if you have encountered this $user->id before 
  • You can issue a get_or_create($user) to store this user information in your DB
  • Start a session on your website for this user

There are many other hairy details like merging accounts, logout etc. However, I hope this post has given you sufficient information to get started.

Update:- 
Here is the code to  process user information

 


function processUser($user) {
    // exisitng record ? find on facebook_id
    // New record - create login + facebook record
    // start login session  
    $id = $user->id;
    $name = $user->name;
    $firstName = $user->first_name ;
    $lastName = $user->last_name ;
    $link = $user->link ;
    $gender = $user->gender ;
    $email = $user->email ;

    // do not know what facebook will return
    // we consider auth to be good enough for a user
    if(empty($name) && empty($firstName)) {
        $name = "Anonymous" ;
    }

    $message = sprintf("Login:Facebook :: id %d ,email %s \n",$id,$email); 
    Logger::getInstance()->info($message);

    $facebookDao = new \com\indigloo\sc\dao\Facebook();
    $loginId = $facebookDao->getOrCreate($id,$name,$firstName,$lastName,$link,$gender,$email);


    if(empty($loginId)) {
        trigger_error("Not able to create login for facebook user",E_USER_ERROR);
    }

    \com\indigloo\sc\auth\Login::startFacebookSession($loginId,$name);
    header("Location: / ");
}




Here you can store your Facebook users in a DB table. There you can check if you already have this Facebook ID. If you are using MYSQL then please do not use int(11) for storing facebook.ID, use string.

You can see a working example at our site : http://www.3mik.com/user/login.php
Questions/Issues/Comments about this post? - send to my blogger-id@gmail