Skip to main content

How to integrate Facebook Connect with a Rails app

·1444 words·7 mins
Author
Yang Chung

Update: I have written up another post for using Devise and Omniauth, and you can find it here. Facebooker is no longer maintained.

Top of my to-do list was to integrate Facebook Connect with my Rails app, since 1) there are A LOT of people using Facebook and 2) having to register to post or edit could be an obstacle in getting more users to use my site.

I looked on the web for a while, and found a great example by Stuart Eccles at Made by Many. It’s an awesome tutorial, but it’s for restful_authentication. I don’t use it, so I had to modify it a little bit. Also, I added a step to ask a user to pick a username. So the following instruction is basically modification of Stuart’s.

Without further ado, let’s begin.

1. Setup Facebook Application page

1.1. Go to this page, and enter a name for your application. I named mine “Playgrounds_R_Us”.

1.2. Make a note of Application ID, API Key, and Secret. You need it for facebooker.yml later.

1.3. Next is Authentication section. Here what’s important is Post-Authroize and Post-Remove Callback URL. They refer to a web page a user will be taken to after logging into and logging out of Facebook account. While testing, I left it at “http://127.0.0.1:3000”.

1.4. I also used the same URL (http://127.0.0.1:3000) for Canvas Callback URL in Canvas section and Connect URL in Connect section.

1.5. Then you are pretty much set with configuration on Facebook side.

2. Get facebooker plugin and install it. From your rails app root directory,

ruby script/plugin install git://github.com/mmangino/facebooker.git

2.1 Configure facebooker.yml. You only need to care about those above “tunnel”. Canvas_page_name could be left blank. Use the keys you noted when you created your Facebook application.

development:
  api_key: [Your KEY]
  secret_key: [Your KEY]
  canvas_page_name: playgroundrus
  callback_url: http://127.0.0.1:3000/
  pretty_errors: true
  set_asset_host_to_callback_url: false
  tunnel:
    public_host_username:
    public_host:
    public_port: 4007
    local_port: 3000
    server_alive_interval: 0
production:
  api_key: [Your KEY]
  secret_key: [Your KEY]
  canvas_page_name: playgroundrus
  callback_url: http://www.playgroundrus.com
  set_asset_host_to_callback_url: false
  tunnel:
    public_host_username:
    public_host:
    public_port: 4007
    local_port: 3000
    server_alive_interval: 0

2.2 Make sure you generate “xd_receiver” files. Without, it won’t work correctly. Also from your rails app root directory,

script/generate xd_receiver

2.3. After that you are all set with facebooker configuration.

3. Prep the database. Add two fields to store facebook id and hashed email so that later we can find a user by either one of the two fields.

3.1. Generate a migration file

ruby script/generate migration AddFbsToUser

3.2. Add fields

def self.up
  add_column :users, :fb_id, :integer
  add_column :users, :email_hash, :string
end

def self.down
  remove_column :users, :fb_id
  remove_column :users, :email_hash
end

3.3. Add those fields with rake call.

rake db:migrate

4. Add Rails functions

4.1. Add the following two lines right after tag in app/views/layouts/application.html.erb.

<%= fb_connect_javascript_tag %>
<%= init_fb_connect "XFBML"%>

4.2. Make sure you have the following line between header tags in app/views/layouts/application.html.erb.

<%= javascript_include_tag :defaults%>

4.3. Also add the followings in the ApplicationController in app/controller/application_controller.rb. When user logs into Facebook account, :facebook_session will have all the information about the user. :facebook_session is utilized a lot.

before_filter :set_facebook_session
helper_method :facebook_session

4.3. Also in ApplicationController, where I see if a user is logged in or not, I had to put additional code to see if it’s a facebook user. It’s a facebook user, if facebook_session is successfully created.

def fetch_logged_in_user
  if facebook_session
    @current_user = User.find_by_fb_user(facebook_session.user)
  else
    return unless session[:user_id]
    @current_user = User.find_by_id(session[:user_id])
  end
end

4.4. Now you can put the following line anywhere in a view, and you will get “Facebook Connect button”.

<%= fb_login_button('window.location = "/users/link_user_accounts";')%>

This will put a nice Facebook Connect button like this,

It could have normal facebook login button options like :size and :background. Check here for the options.

When a user clicks on the button, it will bring up a popup window where user can log in. After the user is logged in, link_user_accounts action in user controller will either create a new user with Facebook credentials or recognize a user as existing user by either email hash code or facebook user id. It will be covered later.

And also make sure the following line is added to the routes in config/routes.rb.

map.resources :users, :collection => {:link_user_accounts => :get}

4.5. In User model in app/models/user.rb, you can just use the same code as Stuart’s.

after_create :register_user_to_fb

#find the user in the database, first by the facebook user id and if that fails through the email hash
def self.find_by_fb_user(fb_user)
  User.find_by_fb_id(fb_user.uid) || User.find_by_email_hash(fb_user.email_hashes)
end

#Take the data returned from facebook and create a new user from it.
#We don't get the email from Facebook and because a facebooker can only login through Connect we just generate a unique login name for them.
#If you were using username to display to people you might want to get them to select one after registering through Facebook Connect
def self.create_from_fb_connect(fb_user)
  new_facebooker = User.new(:username => "fb_#{fb_user.uid}", :password => "", :email => "")
  new_facebooker.fb_id = fb_user.uid.to_i

  #We need to save without validations
  new_facebooker.save(false)
  new_facebooker.register_user_to_fb
end

#We are going to connect this user object with a facebook id. But only ever one account.
def link_fb_connect(fb_id)
  unless fb_id.nil?
    #check for existing account
    existing_fb_user = User.find_by_fb_id(fb_id)

    #unlink the existing account
    unless existing_fb_user.nil?
      existing_fb_user.fb_id = nil
      existing_fb_user.save(false)
    end

    #link the new one
    self.fb_id = fb_id
    save(false)
  end
end

#The Facebook registers user method is going to send the users email hash and our account id to Facebook
#We need this so Facebook can find friends on our local application even if they have not connect through connect
#We hen use the email hash in the database to later identify a user from Facebook with a local user
def register_user_to_fb
  users = {:email => email, :account_id => id}
  Facebooker::User.register([users])
  self.email_hash = Facebooker::User.hash_email(email)
  save(false)
end

def facebook_user?
  return !fb_id.nil? && fb_id > 0
end

But, mine had one difference. Since I didn’t use restful_authentication, my user model was different - no :name and :login fields. So, I changed mine to reflect my model.

new_facebooker = User.new(:username => "fb_#{fb_user.uid}", :password => "", :email => "")

Instead of

new_facebooker = User.new(:name => fb_user.name, :login => "facebooker_#{fb_user.uid}", :password => "", :email => "")

4.6. Add the followings in the User controller in app/controller/users_controller.rb.

def link_user_accounts
  if @current_user.nil?
    #register with fb
    User.create_from_fb_connect(facebook_session.user)
    user = User.find_by_fb_user(facebook_session.user)
    redirect_to edit_user_path(user)
  else
    #connect accounts
    @current_user.link_fb_connect(facebook_session.user.id) unless @current_user.fb_id == facebook_session.user.id
    redirect_to playgrounds_path
  end
end

Here, again mine is a little different, because I wanted to ask user to pick a username instead of temporary “fb_

\[numbers\]

” type of username. So, after a local user account is created, I direct the user to edit_user_path.

4.7. In Edit view of User controller, I see if it has a temporary username or not. If it does, I only ask user to pick a pretty username and perhaps location, which is required when registering in an old way.

<% if @user.facebook_user? && (@user.username.nil? || @user.username =~ /fb_\d/) %>
  <% form_for @user do |f| %>

Please pick a pretty name for PlaygroundRUs: <%= f.text_field :username %>

Where do you live? (Full address, city and state, or just zip): <%= f.text_field :location %>

<%= submit_tag 'Update' %>

  <% end %>
<% else %>
<%= @user.username %>
<% form_for @user do |f| %>

Email:
    <%= f.text_field :email, :value => @user.email %>

    <% if !@user.facebook_user? %>

Password:
      <%= f.password_field :password %>

Password Confirmation:
      <%= f.password_field :password_confirmation %>

    <% end %>

Location:
    <%= f.text_field :location, :value => @user.location %>

    <%= submit_tag 'Update'%>

  <% end %>
<% end %>

4.8. And in Update action of User controller, I make sure the user didn’t pick a duplicate username or email address.

def update
  @user = User.find(params[:id])
  if @user.facebook_user?
    if User.find_by_username(params[:user][:username]) || User.find_by_email(params[:user][:email])
      flash[:error] = "Username or email already exists!"
      redirect_to(edit_user_path) and return
    else
      @user.update_attribute(:username, params[:user][:username]) if params[:user][:username]
      @user.update_attribute(:location, params[:user][:location]) if params[:user][:location]
      @user.update_attribute(:email, params[:user][:email]) if params[:user][:email]
    end
  else
    @user.update_attributes(params[:user])
  end
  redirect_to user_path
end

4.9. For logging out, you can put this anywhere you like. Usually, I put it next to or below facebook icon. The following is in my application view in app/views/layouts/application.html.erb

<% if @current_user.facebook_user? %>

<%= link_to 'Edit', user_path(@current_user) %> | Logout

<% end %>

4.10. Last, but not the least, I found that even if a user is logged out from facebook, its cookie causes the site to think the user is still logged in, and refresh will bring the user back in (from the dead). To kill the ghost, make sure you have the following lines in the Destroy action in Session controller in app/controller/sessions_controller.rb.

def destroy
  if @current_user.facebook_user?
    clear_fb_cookies!
    clear_facebook_session_information
  end
  session[:user_id] = nil
  @current_user = false
  redirect_to playgrounds_path
end

5. When it’s time to deploy, make sure you change the test Callback URL (in my case, http://127.0.0.1:3000) to your production URL (in my case, http://www.playgroundrus.com).