On the Pain of Developing for Facebook

Posted by on Nov 12, 2009 in Blog | Tags: , | 2 Comments

I’m in the last stages of building a site that integrates with Facebook. It includes a Facebook Connect component and a separate Facebook iframe application. In the hopes that some of my experience will help someone else out there, I will share some of my suffering and how I worked through it.

This is a Rails 2.3 application, using the Facebooker gem, version 1.0.54.

Safari, iframes, and cookies.

Chances are high you’ve found this page via search, and that you are desperate to find a solution to allow your Rails sessions to carry over from page #1 of your iframe application to page #2 and beyond. Will Henderson has a solution when your Rails application uses the ActiveRecord session store, but new Rails applications use the cookie store. There is no session to retrieve from the database; passing a row ID with links doesn’t help.

I am fortunate that my iframe application has a splash screen, with only one useful link on it, to start the “real” application. I append every fb_sig parameter from the current request to the link. Once the user clicks that link, the iframe no longer falls under Safari’s 3rd party cookie policy, and so the next page can set cookies. Because all of the fb_sig parameters are there, Facebooker gives precedence to them, recreating the Facebook session without consulting cookies (which either don’t exist or are stale).

<%= link_to 'Start the app', params.merge(:action => :app) %>

Now, your app doesn’t always get these. When the application is first installed, you won’t get the fb_sig parameters. In this case, I force a redirect:

unless params[:fb_sig_ss]
  redirect_to "http://#{Facebooker.canvas_server_base}/#{Facebooker.facebooker_config['canvas_page_name']}/"
  return
end

However…

Facebooker’s Rack middleware trashes your params.

Facebooker includes a piece of Rack middleware that automatically validates the signature on fb_sig parameters, and aborts the request if the signature isn’t valid. This works great on the initial request, but fails on the second request, with exactly the same parameters.

The culprit is near the bottom of Facebook#call, in rack/facebook.rb, where it calls convert_parameters!. This method is trying to be helpful, turning things like Unix time_t into Ruby Time instances. If you are trying to pass these parameters along in a link, Time.to_s is not a Unix time_t, and so the signature validation will fail, and you’re screwed.

Pre-Rails 2.3, there is no Rack middleware, and signature validation happens in another part of the Facebooker gem, which does not trash the original params. It does the same conversion, but on a copy. As near as I can tell, calling convert_parameters! is unnecessary, and commenting it out solves my problem. When Safari loads page #2, Facebooker creates its session, which can be stored in the Rails session, using the cookie store.

Reconstituting a Facebook session in Flash.

The bulk of this iframe application is actually written in Flash. Every Facebook application has an App ID (essentially a public key) and a secret key. The secret key must never be allowed outside your organization or people can fake API requests from your application. Embedding your secret key in a Flash SWF is therefore not an option, because a SWF can be decompiled. Facebook’s solution to this is the session secret, passed as fb_sig_ss.

According to Adobe’s ActionScript 3.0 Client Library for Facebook API reference, one can use the FacebookSessionUtil class to recreate a session using only the public key and the session secret. This is not true. I have to pass in all of the fb_sig parameters to Flash in order to make it work. However you start your Flash content, FlashVars must include:

<%= params.collect { |k,v| "#{k}=#{v}" if k.starts_with?("fb_sig")}.compact.join("&") %>

A refreshed page lags a “logged-in” state change until refreshed a second time.

When a user logs out of Facebook and refreshes your Facebook Connect web site, it doesn’t reflect the user’s “logged-in” state change until another refresh. You can automate this and have the page refresh itself when necessary:

<% init_fb_connect 'XFBML', :app_settings => '{"reloadIfSessionStateChanged":true}' do %>
  FB.ensureInit(function() {
    FB.Connect.get_status().waitUntilReady(function(status) {
      switch (status) {
        case FB.ConnectState.connected:
          $$('.fbConnectLoggedIn').invoke('show');
          $$('.fbConnectLoggedOut').invoke('hide');
          break;
        case FB.ConnectState.appNotAuthorized:
        case FB.ConnectState.userNotLoggedIn:
          $$('.fbConnectLoggedIn').invoke('hide');
          $$('.fbConnectLoggedOut').invoke('show');
          break;
      }
    });
  });
<% end %>

There are actually two parts to this trick. First, :app_settings => '{"reloadIfSessionStateChanged":true}' triggers the automatic refresh.

Second, if you have some common part of your layout that switches on a user’s Facebook status, this precludes your use of page caching. For an otherwise static page, this is a real sacrifice. To work around this, I include both sets of mark-up, classed with either “fbConnectLoggedIn” or “fbConnectLoggedOut”, and invoke Prototype’s show() or hide() on them as part of the Facebook Connect initialization. As long as you are careful to use XFBML tags that don’t require actual values from a Facebook session (e.g. <fb:name uid="loggedinuser" ...>), this works very well.

2 Comments

  1. Mark
    November 20, 2009

    Hey Thanks for the write up. In regards to the Rack params clobbering I’ve put this in a universal before_filter which allows Rack to construct the session again from the params without needing to comment out any code.

    def format_iframe_params(params)
    if ::Rails.version >= “2.3″ and ActionController::Dispatcher.middleware.include? Rack::Facebook

    # Some slight modifications need to be made to these variables are needed to allow
    # the Rack middleware to reconstruct the session.
    params.each_pair do |k,v|
    params[k] = ’1′ if params[k] == true
    params[k] = ’0′ if params[k] == false
    end
    params[:fb_sig_expires] = params[:fb_sig_expires].to_i if params.include?(:fb_sig_expires)
    params[:fb_sig_profile_update_time] = params[:fb_sig_profile_update_time].to_i if params.include?(:fb_sig_profile_update_time)
    params[:fb_sig_time] = params[:fb_sig_time].to_f if params.include?(:fb_sig_time)
    end
    params
    end

  2. Mike Judge
    January 30, 2010

    Thanks for sharing this! It seems to work great.