Thursday, 17 January 2013

Ruby On Rails: Omniauth Devise Authentication using Facebook , Google & Twitter.


This post is just a simple straightforward description of Omniauth:Overview , intended for the beginners , those who wants to try the OmniAuth for the first time with Facebook , Google & Twitter. This post will cover all the basic information i.e From creating the app in Facebook, Google & Twitter for getting the secret key To connect these Apps to your Rails Application.

Things in common :

add Devise & omniauth gem to your Gemfile.

gem 'omniauth' 
gem 'devise' 

First of all install Devise into your application.
rails generate devise:install  
You have to create user model using devise ,
rails g devise user  
and after that we need to add 2 more columns i.e "uid" and 'provider' to our user model.

rails g migration AddColumnsToUsers provider:string uid:string
rake db:migrate
 also dont forget to add ":provider" and ":uid" to your attr_accessible also.

 We have done with the things that is common in authentication using facebook,Google, Twitter.

 We will start with Facebook Authentication first 

 Facebook Authentication :

 add gem 'omniauth-facebook' to your gem file.

 First of all you need to create an application in facebook to get the secret key out .













Next, you need to declare the provider in your (config/initializers/devise.rb) and require it

require "omniauth-facebook"
config.omniauth :facebook, "APP_ID", "APP_SECRET"
if you have done with all the above , you need to make your model (e.g. app/models/user.rb) omniauthable:
devise :omniauthable
Better restart your server to recognize the changes you have made in  Devise Initializer.

Now Devise will create the following url methods.

  • user_omniauth_authorize_path(provider)
  • user_omniauth_callback_path(provide)
use the below line of code in your view file wherever you want to provide the Facebook link to authorize for the users.

<%= link_to "Sign in with Facebook", user_omniauth_authorize_path(:facebook) %>

When the user clicks on the above link, they will redirects to the Facebook login page, after entering their credentials it will again redirect the user back to our applications Callback method . Its time to create Callback method for our application.

To implement the callback method,  first of all we need to again return back to our config/routes.rb file and need to tell Devise , in which controller we are going to create that callback action.
devise_for :users, :controllers => { :omniauth_callbacks => "users/omniauth_callbacks" }
Now we we are going to add a new controller file inside our Rails controller directory "app/controllers/users/omniauth_callbacks_controller.rb" and put the following line code  in your omniauth_callbacks_controller.rb file.

We will start with Facebook provider first .You have to care about one thing that the action name should be the same as the provider name.
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.find_for_facebook_oauth(request.env["omniauth.auth"], current_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
end

What is happening inside this callback action ?
  • When the user enters the credentials in the Facebook sign in form, Facebook actually returns the default information's regarding the particular user , and all that information's retrieved from Facebook by omniauth will be available as a hash at request.env["omniauth.auth"]. try to print this hash for better understanding of what information's are retrieved from Facebook.
  • Then we are actually calling a method in user model (we are going to add soon ) with request hash as an argument.If the user logs in for the first time "find_for_facebook_oauth()" function will create a new entry in our user model,when the new user user has been saved properly in our database, it return that user and assigns to an instance variable @user.
  • If the User model returns a valid user , we should sign in and redirect that user to our application , that is why we are passing the :event => :authentication to the sign_in_and_redirect method to force all authentication callbacks to be called.
  • In case the user is not persisted , we store the omniauth data in the session.Notice we store this data using "devise." as key namespace. This is useful because Devise removes all the data starting with "devise." from the session whenever a user signs in, so we get automatic session clean up. At the end, we redirect the user back to our registration form.
  • Suppose you doesn't want to provide an external registration event (i.e you want to remove the Registerable module from your app ) . you can comment the Registerable module in the devise User model, it will automatically removes all the links and routes to the registration page. and after that we can redirect to root_path if the user is not persisted.
Now we are going to implement the find_for_facebook_oauth method in our user model (e.g. app/models/user.rb) :
def self.find_for_facebook_oauth(auth, signed_in_resource=nil)
    user = User.where(:provider => auth.provider, :uid => auth.uid).first
    if user
      return user
    else
      registered_user = User.where(:email => auth.info.email).first
      if registered_user
        return registered_user
      else
        user = User.create(name:auth.extra.raw_info.name,
                            provider:auth.provider,
                            uid:auth.uid,
                            email:auth.info.email,
                            password:Devise.friendly_token[0,20]
                          )
      end
      
    end
  end

Here you will find some changes compared to the github omniauth documentation.the reason why i have made these changes , you can get it from this stckoverflow question  .

The method above simply tries to find an existing user by using the provider , but if the user is already registered in our application using the same email id but different provider, then we need to restrict the user from creating again the same user entry, otherwise if the user tries to register with the same email account but different provider, Devise will throw error and will restrict the user from signing in to our application. so that is why we are checking again whether the use have already signed in our application using any provider and we have created the user entry in our application. suppose if we finds the registered user , we need to return the registered user, else if the user is not at all registered in our application we are actually creates that user with attributes name, provider, email  and with a random password.

So that is all you want , we have completed the Facebook authentication using omniauth.

Google Authentication :

This is just the same as Facebook authentication, first of all you need to register your app in Google to get the API key. 

You need to update the redirect uri for the API access, if you are using localhost use the following " http://localhost:3000/users/auth/google_oauth2/callback " like shown in the image.



add 'gem 'omniauth-google-oauth2' ' to your Gemfile. then bundle install

require it in 'config/initializers/devise.rb'


require "omniauth-google-oauth2"
config.omniauth :google_oauth2, "APP_ID", "APP_SECRET", { access_type: "offline", approval_prompt: "" }

Add the following link to your view file where you want to put the google sign in option.
<%= link_to "Sign in with Google", user_omniauth_authorize_path(:google_oauth2) %>

When the user clicks on the above link, they will redirects to the Google login page, after entering their credentials it will again redirect the user back to our applications Callback method.

Inside "omniauth_callback_controller"  add the following function "google_oauth2"
class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController
  def google_oauth2
    
    @user = User.find_for_google_oauth2(request.env["omniauth.auth"], current_user)

    if @user.persisted?
      flash[:notice] = I18n.t "devise.omniauth_callbacks.success", :kind => "Google"
      sign_in_and_redirect @user, :event => :authentication
    else
      session["devise.google_data"] = request.env["omniauth.auth"]
      redirect_to new_user_registration_url
    end
  end
end

Inside the user model. "app/models/user.rb"


def self.find_for_google_oauth2(access_token, signed_in_resource=nil)
    data = access_token.info
    user = User.where(:provider => access_token.provider, :uid => access_token.uid ).first
    if user
      return user
    else
      registered_user = User.where(:email => access_token.info.email).first
      if registered_user
        return registered_user
      else
        user = User.create(name: data["name"],
          provider:access_token.provider,
          email: data["email"],
          uid: access_token.uid ,
          password: Devise.friendly_token[0,20]
        )
      end
   end
end

Google Authentication is now ready to use.

Twitter Authentication : 

Important thing in Twitter authentication is Twitter will not provide you email address for any reason , so for the authentications like we did in Facebook and Google, we cant identify a user by using an email whether the user is already registered (i.e have already created in our application) or not.

What i did here is , i have created  a fake email by appending the provider id with the domain string "twitter.com" like "2312234@twitter.com".this will be a unique entry.but this also will not solve our problem, because ones the user have registered in our application for the first time the twitter account , we cant get the email that is for sure , but we are creating one with provider id and domain that is also fine. but we cant identify the user whether he is already registered or not when he/she tries to login for the second time by using Facebook or Google provider.

If you are fine with it you can go forward, i know this is not a good solution. but here we cant leave the column email as blank, we have to enter something in email column when creating a user.

So first of all creates your twitter application here for getting the API key. put the website field as the following "http://127.0.0.1:3000/" if your are using localhost and callback URL also as following "http://127.0.0.1:3000/auth/twitter/callback".


Add the gem gem 'omniauth-twitter' to your Gemfile. and bundle install.

Require it in "config/initializers/devise.rb"
require 'omniauth-twitter'
config.omniauth :twitter ,"APP_ID", "APP_SECRET"


Add the following link to your view file for twitter sign in option.
<%= link_to "Sign in with Twitter", user_omniauth_authorize_path(:twitter) %>

When the user clicks on the above link, they will redirects to the Twitter login page, after entering their credentials it will again redirect the user back to our applications Callback method.

Inside "omniauth_callback_controller" create a function named "twitter" like following,
class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController
  def twitter
    auth = env["omniauth.auth"]
    #Rails.logger.info("auth is **************** #{auth.to_yaml}")
    @user = User.find_for_twitter_oauth(request.env["omniauth.auth"],current_user)
    if @user.persisted?
      flash[:notice] = I18n.t "devise.omniauth_callbacks.success"
      sign_in_and_redirect @user, :event => :authentication
    else
      session["devise.twitter_uid"] = request.env["omniauth.auth"]
      redirect_to new_user_registration_url
    end
  end
end


inside your "User" model, add the following method.
  def self.find_for_twitter_oauth(auth, signed_in_resource=nil)

    user = User.where(:provider => auth.provider, :uid => auth.uid).first
    if user
      return user
    else
      registered_user = User.where(:email => auth.uid + "@twitter.com").first
      if registered_user
        return registered_user
      else
        user = User.create(name:auth.info.name,
          provider:auth.provider,
          uid:auth.uid,
          email:auth.uid+"@twitter.com",
          password:Devise.friendly_token[0,20]
        )
      end
    end
  end

That is all you need about Facebook,Google & Twitter authentication! ,  try it yourself , have fun coding.

18 comments:

  1. Great post, I just want to get google working. I followed all the instructions but I get this error:

    /usr/local/rvm/gems/ruby-1.9.3-p374@seekr/gems/devise-2.2.3/lib/devise/rails/routes.rb:434:in `set_omniauth_path_prefix!': Wrong OmniAuth configuration. If you are getting this exception, it means that either: (RuntimeError)

    1) You are manually setting OmniAuth.config.path_prefix and it doesn't match the Devise one
    2) You are setting :omniauthable in more than one model
    3) You changed your Devise routes/OmniAuth setting and haven't restarted your server

    Any guesses?

    ReplyDelete
  2. Oh so it only happens when I add :omniauthable to my user model, it will start if I have everything else but that.

    ReplyDelete
    Replies
    1. Thank you for your valuable comments ... I hope that your doubts has been cleared...

      Delete
  3. hi, am following this blog to configure facebook authentication with my app, after completing all configuration and trying to login using facebook am getting this error during callback "uninitialized constant Users" what should i do.. could you please help me in this..??

    ReplyDelete
    Replies
    1. Could you please elaborate the Error message , i think you would have missed this line in your route file.

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

      Delete
    2. I have included it in the routes.rb

      this is my omniauth controller

      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.find_for_facebook_oauth(request.env["omniauth.auth"], current_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
      end


      this is my user.rb model

      def self.find_for_facebook_oauth(auth, signed_in_resource=nil)
      user = User.where(:provider => auth.provider, :uid => auth.uid).first
      if user
      return user
      else
      registered_user = User.where(:email => auth.info.email).first
      if registered_user
      return registered_user
      else
      user = User.create(name:auth.extra.raw_info.name,
      provider:auth.provider,
      uid:auth.uid,
      email:auth.info.email,
      password:Devise.friendly_token[0,20],
      )
      end

      end
      end

      this is in my routes.rb file

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

      this is the error i found in logs

      ActionController::RoutingError (uninitialized constant Users::OmniauthCallbacksController):
      activesupport (3.2.9) lib/active_support/inflector/methods.rb:230:in `block in constantize'
      activesupport (3.2.9) lib/active_support/inflector/methods.rb:229:in `each'
      activesupport (3.2.9) lib/active_support/inflector/methods.rb:229:in `constantize'


      i dono where i made mistake

      Delete
    3. check your omniauth_callback controller path . It should be inside users folder like follows "app/controllers/users/omniauth_callbacks_controller.rb"

      Delete
    4. This comment has been removed by the author.

      Delete
  4. I deploy it here, but they don't work. Any instruction for me.

    http://hellorubyonrails.herokuapp.com/

    ReplyDelete
  5. Does you code work for these scenarios ?

    User registers normally, then tries to log in via Omniauth
    User registers via Omniauth, then tries to log in normally

    ReplyDelete
    Replies
    1. In this scenario , There will not be any Normal Registration Process.. Every time user has to sign in by using either FB, Twitter or Google account. No other Custom Registration Forms and Sign in Forms.

      Delete
    2. aahann.. Thanks for you post.
      What I noticed with this solution is, if a user registered with FB, and next time tried to login with google.. the line
      registered_user = User.where(:email => access_token.info.email).first
      will find a user which is used for facebook authentication(considering user uses same email for google and facebook in real life).
      Please note user wanted to authenticate with Google credentials but the user would be allowed to login as he was already registered with FB.
      do you think this is correct , any pointers to handle same email scenario ?

      Delete
  6. How I can implement the multiple user with one app for the twitter?

    ReplyDelete
  7. By adding the provider string directly to the user mode, doesn't that prevent a user from being authenticated by more than one program (I.e. Facebook, Google, and Twitter)?

    ReplyDelete
  8. after google login i have a error: Could not authenticate you from GoogleOauth2 because "Invalid credentials". on my sign in page ? can you please explain

    ReplyDelete
    Replies
    1. Can you follow this link http://myrailsdemo.blogspot.in/ ?

      Delete
  9. I followed your steps , but getting unknown facebook action error ,while am trying to signup with Facebook.

    ReplyDelete
  10. Hi.. while deploying on heroku.. do we have to make any changes to this ??

    ReplyDelete