In this post, I discuss the new and improved Facebook Connect library and how to patch Facebooker to work with its new cookie format.
What new FB Connect Library?
Facebook announced a completely rewritten version of their Facebook Connect Javascript library on November 5th. After playing with it a bit, I’ve become pretty enthusiastic about it, for the following reasons:
- the new library is 10x smaller and 3x faster than its large and sometimes notably slow predecessor
- it has a clean and standardized method of accessing the API via a new universal method that takes single hash and a callback (see examples below)
- no more long lists of hard-to-remember function parameters
- new APIs are available immediately
- the Connect team has done away with the baroque, overgrown architecture of the old library (goodbye, opaque Waitables, assorted sealed classes, and your ilk)
- the documentation is clean, simple, and understandable, albeit somewhat sparse
- the FB object provides an event-based model for handling user session and status changes (see their documentation)
- the geniuses on the Connect team have through some black magic eliminated the need for the local xd_receiver file, cleaning up your site
- the project is open sourced
- it comes with a full suite of publicly-available tests written in qunit
- the enthusiasm the project evokes in a friend on the Facebook Platform team is palpable — clearly, this is the real deal
The new library is still in alpha right now — most notably, there’s no XFBML support, though that’s coming soon. (You can work around that with appropriately-styled buttons bound to the right functions.) The library seems otherwise to be ready for use, as evidenced by sites like theinsider.com which have already implemented the new library in production.
Examples
Here are a few examples of the new library in action, which should help to illustrate the points I made above:
- First, just getting started is a lot cleaner. To initialize Facebook Connect with the new library requires only one line:
FB.init({apiKey: "yourAPIKey"});
Compare that to the old version’s three lines of nested functions and explicit module declarations previously required:
FB_RequireFeatures(["XFBML", "Api", "CanvasUtil"], function(){
FB.Facebook.init("yourApiKey", "/path_to/xd_receiver.htm");
});
- Publishing a stream dialog is a lot cleaner in the new library. Since you specify the options in the hash, it’s easy to understand what’s going on.
// publishing a simple action story to the user's own feed
// signature: publish(Object post, Function cb)
FB.publish({
message: prepopulatedUserMessage,
attachment: streamAttachment,
action_links: anActionLink
}, callbackFunction);
Compare that to the old version, which took up to eight parameters:
// signature: streamPublish(String user_message, Object attachment,
// Object action_links, String target_id, String user_message_prompt,
// Function callback, Boolean auto_publish, String actor_id)
FB.Connect.streamPublish(prepopulatedUserMessage, streamAttachment, anActionLink, null, null, callbackFunction)
- Finally, there’s the universal API method:
FB.api({ method: "fql.query",
query: "SELECT name, pic FROM profile WHERE id=" +
FB.getSession().uid
}, callBackFunction);
FB.api({ method: "pages.isFan",
page_id: 86960763670, // Facebook Connect
uid: FB.getSession().uid
}, callBackFunction);
Compared to the older versions below, these newer calls are more similar, but still easier to understand because the parameters are named in the hash rather than provided as arguments (not to mention the other benefits listed above):
FB.Facebook.apiClient.fql_query("SELECT name, pic FROM user WHERE uid=" + FB.Connect.get_loggedInUser(), callBackFunction);
FB.ApiClient.pages_isFan(86960763670, FB.Connect.get_loggedInUser(), callBackFunction);
See more on the new API method here, midway down the page.
Let’s use it!
Now that you’re, perhaps, convinced, let’s go over how to use the library. As mentioned before, the new Facebook Connect is a complete rewrite, so you need different code to add it to your page:
<div id="fb-root"></div>
<script src="http://static.ak.fbcdn.net/connect/en_US/core.js"></script>
<script>
FB.init({apiKey: "yourAPIKey"});
</script>
That gets you up and running, and you can now determine the user’s status and start making API calls. (We’ll get to that another time.) For now, our goal this blog post is to get Facebooker working with the new library as it did with the old. That way, you can swap the new library into your Rails application without having to change how the app works on the server side.
This focus immediately directs our attention toward one fact: cookies are off by default. This minimizes interference with other libraries that manage session state, but doesn’t work for our purposes; we need to turn cookies back on. Fortunately, that’s easy:
FB.init({apiKey: "yourAPIKey", cookie: true});
Turning on cookies allows us to preserve the existing model and application flows; all we need to update is how the signature is calculated. (The alternative, by the way, is to verify the signature via Ajax and hold onto that information on the server — a valid approach if you want to go that route.)
As I describe how to patch Facebooker to verify a Facebook session, I’m going to avoid getting into Rails-level application decisions related to Facebook; I’ll talk about those in a later blog entry about using Devise with Facebook Connect.
Cookies!
Now that we have cookies enabled, we have to overcome our next challenge: Facebook has improved the cookie format with the new library (see here). Gone is the jar of differently flavored Facebook cookies, each containing an individual parameter. Instead, the platform team is giving us one flavorful cookie that contains as its value the raw string for authentication. Here’s a sample:
"fbs_171e3563d4fee42e0ba27450838bba32"=>"\"expires=1260910800&secret=31WwDz1SBGHijzlfg_tT1A__&session_key=3.7emnRFjwmIazbS38xbD7YA__.86400.1260910800-2905623&sig=c22fc92029080e423d27acb27b5b9b3a&uid=2905623\""
You might notice a couple of things:
- the cookie key is in the format “fbs_#{your_app_key}” (a future cookie with unsigned parameters will be provided as “fb_#{your_app_key}”)
- the cookie value is wrapped in quotes (due to some parsing issues)
- the expected signature is part of the string (we’ll have to remove that)
Verifying the signature’s pretty similar to how it’s done now — take the signed parameters (e.g. the cookie value minus the quotes and the sig), append your application’s secret, and MD5 away. The result should match the value of the sig parameter.
Facebooker
To get Facebooker working, I’ve written five new methods (all but one prefixed by new_) to parse the new format; I’ll walk through those important methods in this entry. In writing this, I’ve tried to stick as closely as possible to the existing Facebooker methods and conventions.
I’ve uploaded the full code here; if you stick that file into your config/initializers, it will add its methods to Facebooker automatically and all will be well.
1) Get the cookie value, eliminating the quotes, so we can manipulate it:
def new_fb_cookie_value
# facebook wraps the cookie string in quotes, which we have to eliminate
(cookies[new_fb_cookie_name] || "").gsub(/\"/, "")
end
def new_fb_cookie_name
"fbs_#{Facebooker.api_key}"
end
2) Process it, verifying all components are present and valid and eliminating the sig from the string:
def new_secure_with_cookies!
# get the cookie
raw_cookie_string = new_fb_cookie_value
# returning raw_string if the cookie isn't set
return unless raw_cookie_string
# since we no longer get individual cookies,
# we have to separate out the components ourselves
parsed = {}
# now generate a list of individual parameters
# as well as the string to use to verify the signature
# the string is the cookie value minus the sig parameter
auth_string = raw_cookie_string.split("&").collect { |parameter|
# parameter is, e.g., expires=1260910800
key_and_value = parameter.split("=")
# save it to the hash
parsed[key_and_value[0]] = key_and_value[1]
# add it back to the string for sig verification
# as long as it's not the expected sig value itself
key_and_value[0] =~ /^sig=/ ? "" : parameter
}.join("&") # preserving the order of the string
# use the hash to verify all components are present
# returning gracefully if any are missing or invalid
return unless parsed["sig"] && parsed['session_key'] && parsed['uid'] && parsed['expires'] && parsed['secret']
return unless Time.at(parsed['expires'].to_s.to_f) > Time.now || (parsed['expires'] == "0")
# if we have the unexpired cookies, verify
# we'll throw an exception if the sig doesn't check out
new_verify_signature(auth_string, parsed["sig"])
@facebook_session = new_facebook_session
@facebook_session.secure_with!(parsed['session_key'], parsed['uid'], parsed['expires'], parsed['secret'])
@facebook_session
end
Note the change to hash keys in several places (“ss” to “secret”, “user” to “uid”).
[I feel the collection block applied to the split string isn't as elegant as I'd like. I'd initially planned to use a sort and join on the hash, as in the original method, but to generate that hash we're already walking through a pre-ordered array made by splitting the string. It seemed easier to take advantage of the existing order and skip the sort. If there's a better way, let me know and I'll gladly improve this with full credit.]
3) Now we have what we need, we actually verify the signature:
def new_verify_signature(auth_string,expected_signature)
# Don't verify the signature if rack has already done so.
# This doesn't change with the new FB Connect library
unless ::Rails.version >= "2.3" and ActionController::Dispatcher.middleware.include? Rack::Facebook
# append the secret to the auth_string we previously made, and MD5
actual_sig = Digest::MD5.hexdigest([auth_string, Facebooker::Session.secret_key].join)
raise Facebooker::Session::IncorrectSignature if actual_sig != expected_signature
end
# don't have to worry about the time exception in the original method
# since time is not passed with the new connect signature
true
end
This is nearly identical to the Facebooker method; we just don’t have to create the string to MD5 from a set of hashed parameters.
4) Alias these methods into the verification process:
def create_facebook_session_with_new_cookies
create_facebook_session_without_new_cookies || new_secure_with_cookies!
end
alias_method_chain :create_facebook_session, :new_cookies
Conclusion
That’s it! Just add that code to your initializers directory, and you’ll be good to go. I’ll post an update once Facebooker provides native support for the new library.
If you’re looking for more Facebook goodness, part 3 of the “Authentication with Devise” series (estimated to arrive in early January) will cover using Devise strategies with Connect authentication. In addition to Devise strategy, the post will cover some broader topics with Facebook Connect (including the challenge of keeping your local state and Facebook’s state in sync.) Stay tuned!
Useful Links