Search code examples
ruby-on-rails-3authenticationsorcery

Rails Sorcery Bug? Creates Duplicate User Accounts


The example sorcery code shown on github appears to me to create duplicate accounts if it is extended to allow for multiple sign in methods (which is the whole point of oauth). You can see in the snipit here that create_from() will be called if login_from() does not succeed.

GITHUB AT at https://github.com/NoamB/sorcery-example-app/blob/master/app/controllers/oauths_controller.rb

def callback
provider = params[:provider]
begin
if @user = login_from(provider)
  redirect_to root_path, :notice => "Logged in from #{provider.titleize}!"
else
  begin
    @user = create_from(provider)

Investigating the source code for create_from in all cases a new User Account record will be created. This would not be correct, if a User account record already exists.

My question: What sorcery methods should be called on the first facebook connect, if a User account has been created by some means other than facebook. login_from will fail, and create_from will generate a duplicate usser record?


Solution

  • Several requests have come through for an answer to this question, so I am providing the answer that Andy Mejia part of my team eventually arrived at for this question. We used the source within sorcery to adapt the following functions:

    # Returns the hash that contains the information that was passed back from Facebook.
    # It only makes sense to call this method on the callback action.
    #
    # Example hash:
    # {:user_info=>{:id=>"562515238", :name=>"Andrés Mejía-Posada", :first_name=>"Andrés", :last_name=>"Mejía-Posada", :link=>"http://www.facebook.com/andmej", :username=>"andmej", :gender=>"male", :email=>"[email protected]", :timezone=>-5, :locale=>"en_US", :verified=>true, :updated_time=>"2011-12-31T21:39:24+0000"}, :uid=>"562515238"}
    def get_facebook_hash
      provider = Rails.application.config.sorcery.facebook
      access_token = provider.process_callback(params, session)
      hash = provider.get_user_hash
      hash.merge!(:access_token => access_token.token)
      hash.each { |k, v| v.symbolize_keys! if v.is_a?(Hash) }
    end
    
    
    # Method added to the User Account model class
    def update_attributes_from_facebook!(facebook_hash)
      self.first_name             = facebook_hash[:user_info][:first_name] if self.first_name.blank?
      self.last_name              = facebook_hash[:user_info][:last_name]  if self.last_name.blank?
      self.facebook_access_token  = facebook_hash[:access_token]
      self.email                ||= facebook_hash[:user_info][:email]
      unless facebook_authentication?
        authentications.create!(:provider => "facebook", :uid => facebook_hash[:uid])
      end
      self.build_facebook_profile if facebook_profile.blank?
      save!
      self.facebook_profile.delay.fetch_from_facebook! # Get API data
    end
    

    To show these code in context, I am also including logic from our controller:

    def callback
       provider = params[:provider]
       old_session = session.clone # The session gets reset when we login, so let's backup the data we need
       begin
         if @user = login_from(provider)   # User had already logged in through Facebook before
           restore_session(old_session)   # Cleared during login
         else
           # If there's already an user with this email, just hook this Facebook account into it.
           @user = UserAccount.with_insensitive_email(get_facebook_hash[:user_info][:email]).first
           # If there's no existing user, let's create a new account from scratch.
           @user ||= create_from(provider) # Be careful, validation is turned off because Sorcery is a bitch!
           login_without_authentication(@user)
         end
       @user.update_attributes_from_facebook!(get_facebook_hash)
       rescue ::OAuth2::Error => e
         p e
         puts e.message
         puts e.backtrace
         redirect_to after_login_url_for(@user), :alert => "Failed to login from #{provider.titleize}!"
         return
       end
       redirect_to after_login_url_for(@user)
     end
    

    I hope this solution is helpful to others.