Converting Rail’s RESTful Authentication to PHP

A few months back we were rebuilding an existing site; the mandate was to use PHP for the upcoming release, but the existing site used Ruby on Rail’s RESTful Authentication gem to encrypt user passwords against a unique salt. Each user had both an encrypted password and a salt value that was being updated every time the user successfully logged in. Pretty smart.

Our problem was that we had to re-create the hashing of the user’s password so existing users could log into the site using their old password. With the code below, we are able to hash the user’s entered password in the same way that the Rails app did, allowing authentication to function properly in the new application. Fortunately the existing application used the default hashing method (we didn’t have access to the old site’s source code), so it was just a matter of digging around GIT to understand how the hashing process worked.

1
2
3
4
5
6
7
8
9
10
11
<?php
// $salt variable as retrieved from user's row based on supplied email address
$salt = '36493cef361b8b180863fe3e2685473f676359df';

$password = $_POST['password'];

//NOTE: this assumes a default Restful Auth setup
$password = sha1('--' . $salt . '--' . $password . '--');

//supplied password should now match encrypted database value if entered correctly
?>

A little jQuery redundancy

It’s kind of absurd to think that one of our sites would be up while Google’s Libraries API is down, but there is a real possibility that the google domain name could be blocked in certain countries and in certain circumstances. So just to be safe, here’s a quick little snippet to make sure it’s loaded either way. Note, this assumes you have jQuery on your webserver.

1
2
3
4
5
6
<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.5.1/jquery.min.js"></script>
<script type="text/javascript">
if (typeof jQuery == 'undefined'){
    document.write(unescape("%3Cscript src='/js/jquery1.5.1.min.js' type='text/javascript'%3E%3C/script%3E"));
}
</script>

Detecting AJAX requests with PHP

In an effort to make client-side code as unobtrusive as possible, we typically take the approach of first building applications without JavaScript.  This practice makes sure all of our anchor tags have legitimate URLs in their href attribute.  We can then attached event handlers to the anchors that should trigger AJAX requests.  Then it’s simply a matter of reading the href attribute of the clicked item, and setting that as the source for the AJAX request.  Nothing new here…

1
2
3
4
5
$('a.trigger').click(function(){
  var src = $(this).attr('href');
  //AJAX request here
  return false;
});

But often we want to fork our controller logic based on whether or not a request was made via AJAX.  For example, no reason getting data we won’t be using in the AJAX response (like navigations, user data, etc).  Plus we want to load AJAX-specific views to return appropriate HTML snippets, or JSON objects.  So we just test to see if $_SERVER['HTTP_X_REQUESTED_WITH'] equals “xmlhttprequest” which seems to work consistently for all of the browsers on our checklist (note, this is sent when an AJAX request is made via jQuery – and most other JavaScript frameworks – but best not to assume).  We set this as a constant in our bootstrap file, allowing us to refer to it throughout our code.

1
define('IS_AJAX', isset($_SERVER['HTTP_X_REQUESTED_WITH']) && strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) == 'xmlhttprequest');

One issue this brought up though was caching of AJAX requests, especially in Internet Explorer.  When updating our HTML with the server’s response, we often saw the entire page load into the target div.  IE was aggressively caching the AJAX request.  Simply setting JQuery to disable caching was the easy fix.

1
$.ajaxSetup({ cache: false });

Another simple approach would be to add a parameter to all of your AJAX requests. And even though the method outlined above is not tamper proof (not that anything really is), it seems a bit more reliable to me than the AJAX parameter approach, especially when you have many hands in the jar during the application development cycle. For more complex application development, the majority of PHP frameworks out there provide this via other means (CakePHP’s RequestHandler for example). But for our home-baked, lightweight Facebook Application frameworks, this works nicely.

Getting FFmpeg running properly on Debian Lenny

We have been using FFmpeg quite a bit lately for a number of projects.  One such project has us encoding uploaded videos into a variety of formats including mpeg4 for iOS devices.  So we fired up FFmpeg, to run a test transcoding with the following command:

1
ffmpeg -i test.mp4 -acodec libfaac -ab 128kb -vcodec mpeg4 -b 1200kb -mbd 2 -flags +4mv+trell -aic 2 -cmp 2 -subcmp 2 -s 320x180 -title X final_video.mp4

We were immediately greeted by an Unknown encoder ‘mpeg4′ error.  There are apparently some licensing issues between ftp-masters and FFmpeg.  Not being a lawyer, I cannot advise on the legality of the following workaround, so this is for informational purposes only.

Turns out we can get ‘full’ installs of FFmpeg from the debian-multimedia repo.  This was as simple as first opening our sources list:

1
$ nano /etc/apt/sources.list

Adding http://www.debian-multimedia.org lenny main, installing the debian multimedia keyring:

1
$ aptitude install debian-multimedia-keyring

Run an upgrade $ aptitude upgrade, and you should be set.  Running that same FFmpeg command output our video.

Add external link tracking with jQuery and Google Analytics

Quick and easy way to setup Google Analytics tracking for all links that open in a new window (typically external links that wouldn’t normally be tracked)… Note, this should be called from within $(document).ready or after all anchor tags have been added to the DOM.

1
2
3
4
5
6
$('a[target=_blank]').click(function(){
    try{
        _gaq.push(['_trackEvent', 'External Links', 'Click', $(this).attr('href')]);
    } catch(err) {}
    return true;
});

A few things to watch out for with Rackspace CloudSites

I was pretty excited when I first heard about Rackspace’s CloudSites product.  The prospect of infinite scaling for just $100/month seemed to good to be true (the cost at the time).  It was.

But that’s not to suggest CloudSites doesn’t have its niche.

Here are a few things to look out for.

It’s horrendously slow

We saw rendering times upward of 1.5 seconds (including database queries) on a very simple application that was rendering in .04 seconds on a relatively underpowered and overworked VPS server.  I can go into greater detail about the VPS configuration if anyone’s interested, but I’ll assume it suffices to say that for hosting any sort of moderately sophisticated database-driven application using a RAD framework, CloudSites should be avoided.  Further to this point, we ran the application from an abysmal shared hosting account (of the $4/month variety) during the same time of day, and that saw rendering times around .2 seconds.

No opcode accelerators

We typically compile PHP with APC support when building servers; no option for that on CloudSites.  No eAccelerator or XCache either.

PHP 5.2.13

This appears to be a recent update to the platform (when we first used the product, they were running 5.2.6).  Still, we much prefer running 5.3 (we currently compile 5.3.5 on our server builds).

No SSH

For smaller projects, we use bash scripts for deployment via rsync.  This allows us to set file permissions, compress CSS / JS, clear cache, and exclude unwanted files with a single command.  Not with CloudSites though.  Fire up that FTP client and don’t make a mistake!

Delicate Media Server / aggressive caching

CloudSites promotes media files (css, js, jpeg, png, etc) to a CDN.  Plan on renaming files if you make updates.  Or setting up a cache-killing solution.

Also, at a pretty inopportune time, all images mysteriously vanished from our site.  I jumped onto online chat for support — I was told the media server was down and would be restored within the hour.  An hour later, no luck.  We subsequently moved all images to a proper CDN (Amazon’s CloudFront).

Damaged databases

Not sure if we are just lucky here, but we have never had a MySQL database ‘break’… until CloudSites.  Suddenly there was a good portion of data missing from the site.  Anything that had not been cached in flat files was gone.  We promptly logged in to the provided PHPMyAdmin GUI and were told that the database had been damaged.  I crossed my fingers and clicked ‘repair’.  Fortunately it worked.  We run scheduled db extracts quite often throughout the day, so it was likely not a huge issue, but just the fact that it happened, for the first time ever, after just a week of using this platform was pretty alarming.

Lost sessions!

This was a big headache… Users reported losing sessions — they would login, browse the site, then after a moment of time, they would no longer be logged in.  Even more alarming, some users reported that they were logged in as different users!  After about 5 hours of digging, we found out that session data is not stored in your home directory, combining it with all other sessions currently being managed by that server.  Okay, makes sense (being generous), but NOWHERE was this documented!  The fix was simple.  Define the session_save_path somewhere within your application.

1
session_save_path('/mnt/datacenter/client_code/youdomain.com/web/sessions/');

This is NOT Rackspace support

Part of the reason we made the premature jump to this platform was because of our experience with Rackspace’s support team in the past.  For almost every issue above, our support technician typically left us with “I don’t know, someone will have to get back to you tomorrow”.  That’s never happened with Rackspace support before.  They will typically ride out any issue with you for as long as it takes.  I tried calling the Cloud Servers support group, but they immediately redirected the call to the CloudSites group.  At one point our technician gave us the proverbial white flag and said simply that it was a new solution, and it was not meant for complex applications.  If running a database makes an application complex…

Keeping connected to the server holding your session

One of our first test cases for CloudSites was a Facebook application.  Big mistake.  Anyone familiar with the platform knows Facebook API requests can take a while to complete, especially methods for uploading photos.  Combine that with the horrendously slow CloudSites platform, and you get 4 image upload requests taking about 30-40 seconds.  Problem is that the cookies that tell your request which server has your session data expire in 6-60 seconds (that’s verbatim what I was told; 6-60 seconds).  In order to keep connected to the server holding your session, you cannot have a request take longer than 6-60 seconds.  This required us to refactor quite a bit, sending data back to the browser at 4 second intervals during this execution, which further slowed down application response time.  Worst of all, sometimes single requests would take more than six seconds (like uploading a 45KB image… um…), and the user would be presented with a “no suitable nodes” found error message.

So what do we use CloudSites for?  It isn’t bad for very simple high traffic applications without any sort of database connectivity.  We can run simple Facebook apps off it and not have to worry about the app suddenly exploding in popularity.  But now that Amazon allows you to define index files on S3, I’m not sure there’s any good reason to keep CloudSites up and running.  Especially at $150/month.

In fairness, we have not run anything substantial on this platform for over a year now (can you blame us?!).  Perhaps they have addressed these issues, or at least communicated them to their users.  But just be very careful about putting anything even remotely important on this solution.

 

Integrating Facebook Connect with CakePHP’s Auth Component

This post is out of date; place check out any of the numerous Cakephp 2+ versions of Facebook Websites implementations out there.

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:

1
2
3
4
5
6
7
8
9
10
<?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:

1
2
3
4
5
6
7
8
9
  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:

1
2
3
4
5
6
7
8
9
10
11
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.

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

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

1
2
3
4
5
6
7
            //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:

1
2
3
4
5
6
7
8
9
            //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.

1
2
3
4
5
6
7
8
            //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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
<?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() &amp;&amp; $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:

1
<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):

1
2
<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.

1
2
3
4
5
6
7
8
9
10
11
<?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.