William Notowidagdo Kiranatama Staff
Knowledge


Here is the case

You are using devise as the authentication solution in a Rails application. Then you need to allow user to merge their Facebook account with their existing account in your application.

Here is the workflow

A user arrive at your home page then she click "Sign in using Facebook", she allow Facebook to connected to your application then she redirected to the sign in page which display a message that she could continue the sign in process and merge her Facebook account with her application account.

Here is how

You should start from here. There you will get some overview and understand the concept. Add omniauth into your Gemfile
gem 'omniauth'
then declare the provider in your config/initializers/devise.rb
config.omniauth :facebook, "<APP_ID>", "<APP_SECRET>"
and make your model omniauthable
devise :omniauthable

Sign in using Facebook

First of all you want to add facebook_uid column to table users so you can store user's Facebook uid there In your config/routes.rb
devise_for :users, :controllers => { :omniauth_callbacks => "users/omniauth_callbacks" }
create the controller to handle the callback app/controllers/users/omniauth_callbacks_controller.rb
class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController
  def facebook
    user = User.find_for_facebook_oauth(env["omniauth.auth"], current_user)
    if user && user.persisted?
      flash[:notice] = I18n.t "devise.omniauth_callbacks.success", :kind => "Facebook"
      sign_in_and_redirect user, :event => :authentication
    else
      session["facebook_data"] = env["omniauth.auth"]
      redirect_to new_user_session_url
    end
  end

end
add this method in your app/models/user.rb
def self.find_for_facebook_oauth(access_token, signed_in_resource=nil)
  data = access_token['extra']['user_hash']
  if user = User.find_by_facebook_uid(data["id"])
    user
  end
end
When user click the 'Sign in with Facebook' link on the Sign in or Sign up page, Facebook authentication and permission page will displayed and user will redirected back to the sign in page. You want to modify the sign in page devise/sessions/new.html.erb
<% if facebook? %>
  <h2>Welcome <%= session["facebook_data"]["user_info"]["first_name"] %>, you almost done</h2>
<% else %>
  <h2>Sign in</h2>
<% end %>
<%= form_for(resource, :as => resource_name, :url => session_path(resource_name)) do |f| %>
  <p><%= f.label :email %><br />
  <%= f.text_field :email %></p>

  <p><%= f.label :password %><br />
  <%= f.password_field :password %></p>

  <% if devise_mapping.rememberable? && !facebook? -%>
    <p><%= f.check_box :remember_me %> <%= f.label :remember_me %></p>
  <% end -%>

  <p><%= f.submit facebook? ? "Merge Account" : "Sign in" %></p>
<% end %>
Did you notice facebook? in the snippet above? it is a simple helper method to detect Facebook session
def facebook?
  session["facebook_data"]
end
Now add this method below (in your ApplicationController) will handle the merge process
def after_sign_in_path_for(resource)
  # Merge data from Facebook with her current account
  if session["facebook_data"] && current_user.facebook_uid.nil?
    current_user.facebook_uid = session["facebook_data"]["uid"]
    current_user.save(:validate => false)
  end
  # Countermeasure against session fixation
  session.keys.grep(/^facebook\./).each { |k| session.delete(k) }

  super
end

Done!

There you are, your users can now sign in using their existing Facebook account and merge it with their application account. Let me know what you think.