Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Omniauth facebook sessions controller create action integration after Michael Hartl tutorial

I have just finished the Michael Hartl tutorial. I am trying to implement omniauth-facebook to sign-up and sign-in users. I am having trouble creating the master variable to use in the create action in the sessions controller. My assumption is that I should put in an if else statement to see if the master is either signing on through facebook or through the default form? Or should I use and or statement:

(master = Master.find_by(email: params[:session][:email].downcase) || Master.from_omniauth(env["omniauth.auth"])) ?

Here is the omniauth-facebook sessons controller create action code:

def create
    user = User.from_omniauth(env["omniauth.auth"])
    session[:user_id] = user.id
    redirect_to root_url
  end

And here is my current sessions controller create action code:

class SessionsController < ApplicationController

    def new
    end

    def create
        master = Master.find_by(email: params[:session][:email].downcase)
        if master && master.authenticate(params[:session][:password])
            sign_in master
            redirect_back_or master
        else
            flash.now[:error] = 'Invalid email/password combination'
            render 'new'
        end
    end
    .
    .
    .


end
like image 612
monty_lennie Avatar asked Dec 21 '13 13:12

monty_lennie


2 Answers

Whilst I might be late by one year to answer this question, I still think that new viewers with the same sort of problem like you had a year ago might benefit from the following answer

As I understand it the actual question is:

How does one integrate Facebook Authentication with an Authentication system that is built from Scratch?

Since you said you used Michael Hartl's tutorial I went ahead and adapted my answer to an authentication system written by Michael himself. Now let's go over each piece of code one step at a time.


1

First of all we need to create two additional columns to our existing Users table for the provider and uid as follows...

rails g migration add_facebook_auth_to_users provider uid

...and add omniauth-facebook gem to the Gemfile...

Gemfile

gem 'omniauth-facebook', '2.0.1'

Don't forget to run bundle install and migrate your database after.


2

Next we would need to go ahead and edit our Sessions controller...

sessions_controller.rb

def create
  user = User.find_by(email: params[:session][:email].downcase)
  if user && user.authenticate(params[:session][:password])
    if user.activated?
      log_in user
      params[:session][:remember_me] == '1' ? remember(user) : forget(user)
      redirect_back_or user
    else
      message  = "Account not activated. "
      message += "Check your email for the activation link."
      flash[:warning] = message
      redirect_to root_url
    end
  else
    flash.now[:danger] = 'Invalid email/password combination'
    render 'new'
  end
end

def create_facebook
  user = User.from_omniauth(env["omniauth.auth"])
  log_in user
  redirect_back_or user 
end

Sessions controller above contains two create actions. First one is the untouched method taken straight from the book, when the second one is a piece of code I used specifically for Facebook authentications.

Line user = User.from_omniauth(env["omniauth.auth"]) has two purposes. Create a user IF his/her unique facebook uid is not in the database ELSE log this person in the app if the uid exists. More on this later.


3

Next lets create a working .from_omniauth method that you briefly showed inside of your code in your question...

user.rb

def self.from_omniauth(auth)
  where(provider: auth.provider, uid: auth.uid).first_or_initialize.tap do |user|
    user.provider = auth.provider
    user.uid = auth.uid
    user.name = auth.info.name unless user.name != nil
    user.email =  SecureRandom.hex + '@example.com' unless user.email != nil
    user.activated = true
    user.password = SecureRandom.urlsafe_base64 unless user.password != nil
    user.save!
  end
end

Here we try to find a user that matches the given provider and uid values from the hash and then we call first_or_initialize on the result of this. This method will either return the first matching record or initialize a new record with the parameters that were passed in. We then call tap on this to pass that User instance to the block. Inside the block we set various attributes on the user based on values from the OmniAuth hash.

You might be interested as to why did I put the unless statement after some of the attribute initializations. Well consider the situation whereby the user wants to update the profile, like change the name or smth, and then logs out. When that user finally decides to log back in, the .from_omniauth method will overwrite the User's in question update to the original facebook values, unless we stop it from doing so.

Also notice the use of SecureRandom library. Inside of the traditional authentication that was used by Michael Hartl inside his book, he introduces validations to email and password submissions. Emails must be neither blank nor taken. Likewise, a password has to be greater than 6 characters in length. Since emails have to be present and unique I decided to create dummy emails using SecureRandom.hex + @example.com. This will create a random hexadecimal string, like 52750b30ffbc7de3b362, and append it to @example.com, hence generating the unique dummy email. Same goes for the password, however, I preferred to generate a random base64 string using SecureRandom.urlsafe_base64. The most important thing to remember that Facebook users don't need to know this information to login since that is the whole point of using Facebook authentication. This allows them to add this information with real data later on if they desire to do so.


4

Now would be a good time to add the button on the main application page for users to let them actually log in into the app...

_header.html.erb

<% if logged_in? %>
  .
  .
  .
<% else %>
  .
  .
  <li><%= link_to "Sign in with Facebook", "/auth/facebook", id: "sign_in" %></li>
<% end %>

5

So, now we can send a request to facebook. One problem remains that we cannot actually handle the callback. To do so we need to do the following...

  • Add get 'auth/:provider/callback' => 'sessions#create_facebook' to the routes.rb file

  • Use convenient Javascript SDK provided by Facebook. Let's create a custom javascript file that uses this SDK...

app/assets/javascripts/facebook.js.erb

jQuery(function() {
  $('body').prepend('<div id="fb-root"></div>');
  return $.ajax({
    url: window.location.protocol + "//connect.facebook.net/en_US/sdk.js",
    dataType: 'script',
    cache: true
  });
});

window.fbAsyncInit = function() {
  FB.init({
    appId: 'your_facebook_app_id_goes_here',
    version: 'v2.3',
    cookie: true
  });
  $('#sign_in').click(function(e) {
    e.preventDefault();
    return FB.login(function(response) {
      if (response.authResponse) {
        return window.location = '/auth/facebook/callback';
      }
    });
  });
  return $('#sign_out').click(function(e) {
    FB.getLoginStatus(function(response) {
      if (response.authResponse) {
        return FB.logout();
      }
    });
    return true;
  });
};

6

And that is it, except for one small caveat...

problem

If user decides to go and edit the profile, he/she would see our dummy email displayed right in the middle.This happends because Rails is smart enough to autofill User's information.In this case this, frankly, is quite embarassing. Good thing, however, that it is an easy fix. Just set the value to nil inside the form, like so...

<%= f.email_field :email, value: nil, class: 'form-control' %>


Hopefully this helps people who would like to go the route of building authentication systems from scratch :)

like image 139
Timur Mamedov Avatar answered Nov 02 '22 11:11

Timur Mamedov


Ok I was trying to integrate omniauth with the custom authorization code from the tutorial. After some research I decided to remove all the tutorial authorization code and go with Devise instead.

like image 45
monty_lennie Avatar answered Nov 02 '22 11:11

monty_lennie