From 6a3f2f73d576e9672d6cd990e0e0aa0e288bc8cf Mon Sep 17 00:00:00 2001 From: John Bintz Date: Fri, 17 Sep 2010 13:12:58 -0400 Subject: [PATCH] canvas app work, need to write tests for it --- README | 39 ++++++++++++- lib/facebooker2.rb | 22 +++++--- lib/facebooker2/oauth_exception.rb | 4 ++ lib/facebooker2/rails/controller.rb | 37 +++++++------ .../rails/controller/canvas_oauth.rb | 55 +++++++++++++++++++ spec/rails/controller/canvas_oauth_spec.rb | 0 6 files changed, 129 insertions(+), 28 deletions(-) create mode 100644 lib/facebooker2/oauth_exception.rb create mode 100644 lib/facebooker2/rails/controller/canvas_oauth.rb create mode 100644 spec/rails/controller/canvas_oauth_spec.rb diff --git a/README b/README index 074479b..454934e 100644 --- a/README +++ b/README @@ -36,7 +36,7 @@ shared login partial. <%= fb_connect_async_js %> <% if current_facebook_user %> <%= "Welcome #{current_facebook_user.first_name} #{current_facebook_user.last_name}!" %> - or + or <%= "Hello #{fb_name(current_facebook_user, :useyou => false)}!" # link to facebook profile %> <%= fb_logout_link("Logout of fb", request.url) %>
@@ -48,4 +48,41 @@ shared login partial. <%= fb_login_and_redirect('', :perms => 'email,user_birthday') %> <% end %> +Using with Canvas Applications +============================== + +To improve integration with Facebook and iframe canvas applications, the primary goal +being things like FB.ui work as a dialog rather than a popup, the application +needs to be authenticated against the user's account via OAuth before use. + +0. Prerequisite: You need a Facebook app. Have your canvas page name handy. + +1. Install facebooker2. + +2. Create config/facebooker.yml as above, but add the following key: + +production: + canvas_page_name: + +3. Add the following lines to your app/controllers/application_controller.rb + +include Facebooker2::Rails::Controller::CanvasOAuth + +ensure_canvas_connected_to_facebook :oauth_url, 'publish_stream' +create_facebook_oauth_callback :oauth + +rescue_from Facebooker2::OAuthException do |exception| + redirect_to 'http://www.facebook.com/' +end + +4. Create a route that generates a URL for the OAuth callback and calls the appropriate +action on your controller: + +map.oauth '/oauth', :controller => :application, :action => :oauth + +5. Your canvas application will now ensure that the current user has authorized +the application before anything else is allowed. The authorization and FB.ui dialogs +will appear inline instead of as popups, improving user experience. + Copyright (c) 2010 Mike Mangino, released under the MIT license +Copyright (c) 2010 John Bintz, released under the MIT license diff --git a/lib/facebooker2.rb b/lib/facebooker2.rb index 3d4e793..2695982 100644 --- a/lib/facebooker2.rb +++ b/lib/facebooker2.rb @@ -1,35 +1,37 @@ # Facebooker2 require "mogli" + module Facebooker2 class NotConfigured < Exception; end class << self - attr_accessor :api_key, :secret, :app_id + attr_accessor :api_key, :secret, :app_id, :canvas_page_name end - + def self.secret - @secret || raise_unconfigured_exception + @secret || raise_unconfigured_exception end - + def self.app_id @app_id || raise_unconfigured_exception end - + def self.raise_unconfigured_exception raise NotConfigured.new("No configuration provided for Facebooker2. Either set the app_id and secret or call Facebooker2.load_facebooker_yaml in an initializer") end - + def self.configuration=(hash) self.api_key = hash[:api_key] self.secret = hash[:secret] self.app_id = hash[:app_id] + self.canvas_page_name = hash[:canvas_page_name] end - + def self.load_facebooker_yaml config = YAML.load(File.read(File.join(::Rails.root,"config","facebooker.yml")))[::Rails.env] raise NotConfigured.new("Unable to load configuration for #{::Rails.env} from facebooker.yml. Is it set up?") if config.nil? self.configuration = config.with_indifferent_access end - + def self.cast_to_facebook_id(object) if object.kind_of?(Mogli::Profile) object.id @@ -43,8 +45,10 @@ end require "facebooker2/rails/controller" +require "facebooker2/rails/controller/canvas_oauth" require "facebooker2/rails/helpers/facebook_connect" require "facebooker2/rails/helpers/javascript" require "facebooker2/rails/helpers/request_forms" require "facebooker2/rails/helpers/user" -require "facebooker2/rails/helpers" \ No newline at end of file +require "facebooker2/rails/helpers" +require "facebooker2/oauth_exception" diff --git a/lib/facebooker2/oauth_exception.rb b/lib/facebooker2/oauth_exception.rb new file mode 100644 index 0000000..b588a86 --- /dev/null +++ b/lib/facebooker2/oauth_exception.rb @@ -0,0 +1,4 @@ +module Facebooker2 + class OAuthException < StandardError; end +end + diff --git a/lib/facebooker2/rails/controller.rb b/lib/facebooker2/rails/controller.rb index e69dc08..66fb886 100644 --- a/lib/facebooker2/rails/controller.rb +++ b/lib/facebooker2/rails/controller.rb @@ -3,31 +3,31 @@ require "hmac-sha2" module Facebooker2 module Rails module Controller - + def self.included(controller) controller.helper Facebooker2::Rails::Helpers controller.helper_method :current_facebook_user controller.helper_method :current_facebook_client controller.helper_method :facebook_params end - + def current_facebook_user fetch_client_and_user @_current_facebook_user end - + def current_facebook_client fetch_client_and_user @_current_facebook_client end - + def fetch_client_and_user return if @_fb_user_fetched fetch_client_and_user_from_cookie fetch_client_and_user_from_signed_request unless @_current_facebook_client @_fb_user_fetched = true end - + def fetch_client_and_user_from_cookie app_id = Facebooker2.app_id if (hash_data = fb_cookie_hash_for_app_id(app_id)) and @@ -35,20 +35,20 @@ module Facebooker2 fb_create_user_and_client(hash_data["access_token"],hash_data["expires"],hash_data["uid"]) end end - + def fb_create_user_and_client(token,expires,userid) client = Mogli::Client.new(token,expires.to_i) user = Mogli::User.new(:id=>userid) - fb_sign_in_user_and_client(user,client) + fb_sign_in_user_and_client(user,client) end - + def fb_sign_in_user_and_client(user,client) user.client = client @_current_facebook_user = user @_current_facebook_client = client @_fb_user_fetched = true end - + def fb_cookie_hash_for_app_id(app_id) return nil unless fb_cookie_for_app_id?(app_id) hash={} @@ -59,15 +59,15 @@ module Facebooker2 end hash end - + def fb_cookie_for_app_id?(app_id) !fb_cookie_for_app_id(app_id).nil? end - + def fb_cookie_for_app_id(app_id) cookies["fbs_#{app_id}"] end - + def fb_cookie_signature_correct?(hash,secret) sorted_keys = hash.keys.reject {|k| k=="sig"}.sort test_string = "" @@ -77,13 +77,13 @@ module Facebooker2 test_string += secret Digest::MD5.hexdigest(test_string) == hash["sig"] end - + def fb_signed_request_json(encoded) chars_to_add = 4-(encoded.size % 4) encoded += ("=" * chars_to_add) Base64.decode64(encoded) end - + def facebook_params @facebook_param ||= fb_load_facebook_params end @@ -93,20 +93,21 @@ module Facebooker2 sig,encoded_json = params[:signed_request].split(".") return {} unless fb_signed_request_sig_valid?(sig,encoded_json) ActiveSupport::JSON.decode(fb_signed_request_json(encoded_json)).with_indifferent_access - end - - def fb_signed_request_sig_valid?(sig,encoded) + end + + def fb_signed_request_sig_valid?(sig,encoded) base64 = Base64.encode64(HMAC::SHA256.digest(Facebooker2.secret,encoded)) #now make the url changes that facebook makes url_escaped_base64 = base64.gsub(/=*\n?$/,"").tr("+/","-_") sig == url_escaped_base64 end - + def fetch_client_and_user_from_signed_request if facebook_params[:oauth_token] fb_create_user_and_client(facebook_params[:oauth_token],facebook_params[:expires],facebook_params[:user_id]) end end + end end end diff --git a/lib/facebooker2/rails/controller/canvas_oauth.rb b/lib/facebooker2/rails/controller/canvas_oauth.rb new file mode 100644 index 0000000..3d7f602 --- /dev/null +++ b/lib/facebooker2/rails/controller/canvas_oauth.rb @@ -0,0 +1,55 @@ +module Facebooker2 + module Rails + module Controller + module CanvasOAuth + def self.included(controller) + controller.extend(CanvasOAuthClass) + + class << controller + attr_accessor :_facebooker_oauth_callback_url, :_facebooker_scope + end + end + + protected + def canvas_oauth_connect + raise "Canvas page name not defined! Define it in config/facebooker.yml as #{::Rails.env}: canvas_page_name: ." if !Facebooker2.canvas_page_name + if params[:error] + raise Facebooker2::OAuthException.new(params[:error][:message]) + else + redirect_to ('http://apps.facebook.com/' + Facebooker2.canvas_page_name) if params[:code] + end + return false + end + + def ensure_canvas_connected + case self.class._facebooker_oauth_callback_url + when Symbol + callback_url = send(self.class._facebooker_oauth_callback_url) + end + + if current_facebook_user == nil && !params[:code] && !params[:error] + render :text => "" + return false + end + end + end + + module CanvasOAuthClass + def ensure_canvas_connected_to_facebook(oauth_callback_url, *scope) + self._facebooker_oauth_callback_url = oauth_callback_url + self._facebooker_scope = scope + + before_filter :ensure_canvas_connected + end + + def create_facebook_oauth_callback(method_name) + self.class_eval(<<-EOT) + def #{method_name} + return canvas_oauth_connect + end + EOT + end + end + end + end +end diff --git a/spec/rails/controller/canvas_oauth_spec.rb b/spec/rails/controller/canvas_oauth_spec.rb new file mode 100644 index 0000000..e69de29