Devise Facebook Omniauth login with connect and disconnect functionality

When talking about users authentication in Rails land there is one name that generally stands above all the other available gems. This name is Devise.

I would not be so wrong to call it the de facto standard of Rails authentication considering the time has been around and the vast documentation it has under its belt.

Regarding for example the OAuth2 functionality there is a well documented page inside the wiki that describes how to implement it for Facebook.

Unfortunately what presented inside the documentation doesn’t always blend well with the other functionalities of an application.

In Devise documentation however there is a fundamental part regarding the implementation of the facebook callback that is required to register/retrieve the user and handle his sign in. This is done, as you can see, by overriding the Devise::OmniauthCallbacksController.


class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController 
  def facebook # You need to implement the method below in your model (e.g. app/models/user.rb) 
    @user = User.from_omniauth(request.env["omniauth.auth"]) 
    if @user.persisted? 
      sign_in_and_redirect @user, :event => :authentication #this will throw if @user is not activated
      set_flash_message(:notice, :success, :kind => "Facebook") if is_navigational_format?
    else
      session["devise.facebook_data"] = request.env["omniauth.auth"]
      redirect_to new_user_registration_url
    end
  end

  def failure
    redirect_to root_path
  end
end

After overriding the controller code the documentation dives into the implementation of the from_omniauth method inside the User model.  Here’s the implementation of the method at the time I’m writing this article:


def self.from_omniauth(auth)
  where(provider: auth.provider, uid: auth.uid).first_or_create do |user|
    user.email = auth.info.email
    user.password = Devise.friendly_token[0,20]
    user.name = auth.info.name   # assuming the user model has a name
    user.image = auth.info.image # assuming the user model has an image
    # If you are using confirmable and the provider(s) you use validate emails, 
    # uncomment the line below to skip the confirmation emails.
    # user.skip_confirmation!
  end
end

The main logic behind it is the retrieval or creation of a user (i.e. first_or_create) by the name of the provider and uid returned by the authentication service (i.e. auth). In case of creation the user gets set the email, password, name and image. If there is the need to store additional data for the user, e.g. the surname, you can simply set the attribute right after the last one present in the previous snippet of code.  If the user is already registered with the given provider and uid it is simply retrieved and returned.

Unfortunately this kind of implementation doesn’t let you handle some additional functionality  you may need to add to your application, for example the possibility to manually connect and disconnect the user from his Facebook account.

Recently I was notified about of a bug related exactly to the over mentioned functionality. After a user disconnects his account from Facebook he can not login again through Facebook Oauth.

The problem was indeed related to over mentioned User::from_omniauth implementation. In particular it wasn’t considering all the possible user states that the application could generate, like for example a user with a valid account that had previously logged in with Facebook and then disconnected his account from it.

In order to handle the over mentioned user states I should have added some more logic to the User::from_omniauth suggested by the Devise documentation.

To avoid the pollution of this method, however, I decided to extract the logic (i.e. the handling of users connecting and disconnecting from their Facebook account) from it. As a result I ended up implementing a simple PORO (…service object?…) that could be used inside the override of the custom Devise::OmniauthCallbacksController. Here there is the PORO:


class UserFromFacebookAuth
  attr_reader :auth

  def initialize auth
    @auth = auth
  end

  def user
    if user = User.find_by(provider: auth.provider, uid: auth.uid)
      user
    elsif user = User.find_by(email: auth.info.email, provider: nil, uid: nil)
      user.tap { |o| o.update attributes_from_auth }
    else
      User.create attributes_from_auth
    end
  end

  private

  def attributes_from_auth
    {
      provider:      auth.provider,
      uid:           auth.uid,
      facebook_name: auth.info.name,
      name:          auth.info.first_name,
      surname:       auth.info.last_name,
      email:         auth.info.email,
      password:      Devise.friendly_token[0, 20],
      privacy_a:     false,
      privacy_b:     false
    }
  end
end

and here the updated version of the Devise::OmniauthCallbacksController:


class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController def facebook # The new PORO! @user = UserFromFacebookAuth.new(auth).user if @user.persisted? sign_in_and_redirect @user, :event => :authentication #this will throw if @user is not activated
      set_flash_message(:notice, :success, :kind => "Facebook") if is_navigational_format?
    else
      session["devise.facebook_data"] = request.env["omniauth.auth"]
      redirect_to new_user_registration_url
    end
  end

  def failure
    redirect_to root_path
  end
end

I find the logic quite simple to follow and understand so I will not dive into its explanation.

The point that I want to stress out in this article may sound silly but I think it deserves some attention: online documentation is useful (in case of Devise even more!) but remains a “documentation”. It should not be simply “used” but “understood” and adapted to each specific case.

To put it simple: copy&paste? Yes but please, try to understand what you’re doing! 😉

Cheers!

Leave a Reply

wpDiscuz