3

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?

Dan Oblinger
  • 489
  • 3
  • 15
  • This is not a bug, but the functionality you're looking for (add authentications to existing user) seems not to be really supported by Sorcery right now. I would love to hear from anyone who has an example of how to patch that in to an app. – Andrew Apr 30 '12 at 03:34
  • @ChristianFazzini we did have luck. I posted our approach in this thread. – Dan Oblinger Sep 30 '12 at 08:46

3 Answers3

3

You can use def create_and_validate_from(provider).

It will validate if the users email/username already exist. If its true, that he will store infos into a session and can be rendered into registration form.

And if you wish to add some provider to your account you can use def add_provider_to_user(provider).

j0k
  • 22,600
  • 28
  • 79
  • 90
nxxn
  • 329
  • 3
  • 14
  • I cannot vouch for this answer -- I do not believe it existed when we constructed our solution. If accurate, this approach is obviously superior. – Dan Oblinger Sep 30 '12 at 08:48
2

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=>"andmej@gmail.com", :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.

Dan Oblinger
  • 489
  • 3
  • 15
0

I came across the same problem. While I have not found a direct solution via Sorcery, I did the following which seems to work:

    @user = create_from(params[:provider]) do |user|
      User.where(:twitter_id => user.twitter_id).first.blank?
    end

This teqnique requires that you have twitter_id in the User model. You can also do it the other way around with the Authentication model instead. Such as:

    @user = create_from(params[:provider]) do |user|
      Authentication.where(:uid => user.twitter_id).first.blank?
    end

If the block returns false, then it doesn't create the user. Avoiding any duplicates.

Note, the block for create_from does not work with 0.7.12. It works with 0.7.13.

Christian Fazzini
  • 19,613
  • 21
  • 110
  • 215