Social

Social

Social

Friday, December 2, 2016

Facebook Authentication in Ruby on Rails

Goal:  Make my site easier for vistors by allowing them to register or login (existing Devise account) with Facebook.

I have Rails 4.2, Devise 4.2

I mostly follow the this Devise how-to for setting up OmniAuth:
https://github.com/plataformatec/devise/wiki/OmniAuth:-Overview


Exceptions to above how-to:

in devise.rb initializer I added

 # added this for facebook authentication
  if Rails.env == "production"
    fb_callback = "https://www.mylivesite.com/users/auth/facebook/callback" 
  else 
    fb_callback = "http://localhost:3000/users/auth/facebook/callback" 
  end
  config.omniauth :facebook, ENV['AD_FB_APP_ID'], ENV['AD_FB_APP_SECRET'],
                callback_url: fb_callback ,
                info_fields: 'name,email'
The environment variables are
AD_FB_APP_ID and AD_FB_APP_SECRET I set these in my local machine's bashrc file (ubuntu linux installed) to match the values in my newly created facebook app.


my projects are located under /home/sean/projects, so when I am in a specific project directory I edit bashrc like:

nano ../../.bashrc

re-read bashrc after saving:  source ../../.bashrc

Facebook App Dashboard


Facebook settings are tricky. Within https://developers.facebook.com/apps/xxxxxxxxxxxxxxx/dashboard/


Add Facebook Login product


Then Set 'Valid OAuth redirect URIs' to:

http://localhost:3000 and https://www.mylivesite.com


Under Settings: Add 'website' platform, then set app domains to www.mylivesite.com

Under App Review: make live


User Methods


After following along with the rest of the devise tutorial above, I came up with the following two methods for my user model:
  def self.from_omniauth(auth)
    user = User.find_by(email: auth.info.email)
    # https://github.com/plataformatec/devise/wiki/How-To:-Add-:confirmable-to-Users
    if user and user.confirmed? 
      user.provider = auth.provider
      user.uid = auth.uid
      return user
    end
    #where(auth.slice(:provider, :uid)).first_or_create do |user|
    where(provider: auth.provider, uid: auth.uid).first_or_create do |user|
     #user.skip_confirmation! 
      user.provider = auth.provider
      user.uid = auth.uid
      user.email = auth.info.email
      user.password = Devise.friendly_token[0,20]
      user.first_name = auth.info.name.split(" ")[0..-2].join(" ")
      user.last_name = auth.info.name.split(" ").last
      #user.first_name = first_name
      #user.last_name = last_name
    end
  end

  def self.new_with_session(params, session)
    super.tap do |user|
      if data = session["devise.facebook_data"] && session["devise.facebook_data"]["extra"]["raw_info"]
        user.email = data["email"] if user.email.blank?
        user.first_name = data["name"].split(" ")[0..-2].join(" ") if user.first_name.blank?
        user.last_name = data["name"].split(" ").last if user.last_name.blank?
      end
    end

  end

I am using Heroku so I need to remember to set the Facebook app key and secret variables in my Heroku settings as well

Update


I realized I need users to finish filling out their registration form to get info that facebook does not provide such as phone.  However, since the password was auto generated for them on registration with facebook, I need a way to get the additional info and not bother them for a password in the process (which they do not know).

First I redirect the new signup to the registration page:
I edit my omniauths_callbacks_controller.rb

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 @user

      # there is probably a better way to do this but I need to get new accounts to finish registration
      if @user.created_at > Time.now - 1.minute 
        redirect_to edit_user_registration_url 
        flash[:alert] = "Please set phone.  Choose texting preference if desired."
      else
        redirect_to root_url

      end

      #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?
       flash[:alert] = "Please set phone.  Choose texting preference if desired."
    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

Then, I add a new controller under controllers/users/registrations_controller.rb:

class Users::RegistrationsController < Devise::RegistrationsController
  # allow facebook registrations to edit account without password
  # http://stackoverflow.com/questions/13436232/editing-users-with-devise-and-omniauth

  def update_resource(resource, params)
    resource.update_with_password(params)
  end

  # Overwrite update_resource to let users to update their user without giving their password
  def update_resource(resource, params)
    if resource.provider == "facebook"
      params.delete("current_password")
      resource.update_without_password(params)
    else
      resource.update_with_password(params)
    end
  end
end

I add a route for it in routes.rb like so:

devise_for :users, :controllers => { :omniauth_callbacks => "users/omniauth_callbacks", :registrations => 'users/registrations' }

Lastly, I hide the password fields on my views/devise/registrations/edit.html.rb form by wrapping all three of the password fields with

<% if (!current_user.provider=='facebook') %>

<% end %>