Setting up User Authentication in Rails (Devise, Facebook,Twitter,Google)

I this Tutorial we will create a new rails application in which we will setup social authentication.

We are starting with implementation of user authentication in rails with Devise later followed by implementing social authentication options for users using omniauth.

Basic Application Setup:

rails new social_auth_demo

## Gemfile


## gems for authentication
gem 'devise'
gem 'devise-async'
gem 'omniauth-facebook'
gem 'omniauth-google-oauth2'
gem 'omniauth-oauth2', '~> 1.3.1'
gem 'omniauth-twitter'
gem 'twitter'
gem 'google-api-client', '0.8.2', require: 'google/api_client'

Now we need to run the devise generators.
we will also copy the devise views as we will be doing further modifications on few of the pages.

rails generate devise:install
devise:install

copies over config/initializers/devise.rb and a localized message file.
Follow the outputed instuctions to setup flashes and mailer configs.

rails generate devise User
rails generate devise:views

Lets create a basic controller to see if our login works

rails g controller welcome index
rake db:create
rake db:migrate

##routes.rb

get 'welcome/index'
root 'welcome#index'

WeclomeController should now look like

## app/controllers/welcome_controller.rb

class WelcomeController
  before_action :authenticate_user!

  def index
  end
end

## app/views/welcome/index.html.erb
<%= link_to “Signout”, destroy_user_session_path, method: :delete %>

The method: :delete part is something we usually forget about.
Now test the application for normal devise authentication

Configure Omniauth

##config/initializers/devise.rb

config.omniauth :google_oauth2, 'APP_ID', 'APP_SECRET', scope: 'user,public_repo'
config.omniauth :facebook, 'APP_ID', 'APP_SECRET', scope: 'user,public_repo'
config.omniauth :twitter, 'APP_ID', 'APP_SECRET', scope: 'user,public_repo'

replace 'APP_ID' withyour app id
replace 'APP_SECRET' withyour application secret key
set scopes as per application requirement

We pass in scopes to a few strategies, which is where we can configure omniauth to request specific permissions.
Sometimes you need to enable them on the remote side before you can request things (e.g. google, twitter) so make sure that things are setup there.

Tell Devise about omniauthable

##app/models/user.rb

devise :database_authenticatable, :registerable,:confirmable,
:recoverable, :rememberable, :trackable, :omniauthable,:validatable,
:omniauth_providers => [:facebook,:google_oauth2,:twitter]

Create Identity model to store access_keys and metadata

Now we are ready to plug in oauth authentications. The flow is:
1. User requests /users/auth/:provider, where provider one of the strategies that you defined above.
2. Omniauth does magic and directs the user to the remote service.
3. The user grants us access and is redirected to the callback path.
4. The OmniauthCallbacks controller is called on our application with the relavent info.
We will use this info to create the user. We are also going to store it to be able to access the service on behalf of the user, and we’ll need to store the access_token in order to do so.
Google is slightly more complicated and we’ll need to store a refresh_token as well.

rails g model identity user:references provider:string accesstoken:string refreshtoken:string uid:string name:string email:string nickname:string image:string phone:string urls:string
rake db:migrate

#app/models/identity.rb

class Identity < ActiveRecord::Base
  belongs_to :user
  validates_presence_of :uid, :provider
  validates_uniqueness_of :uid, :scope => :provider

  def self.find_for_oauth(auth)
    identity = find_by(provider: auth.provider, uid: auth.uid)
    identity = create(uid: auth.uid, provider: auth.provider) if identity.nil?
    identity.accesstoken = auth.credentials.token
    identity.refreshtoken = auth.credentials.refresh_token
    identity.name = auth.info.name
    identity.email = auth.info.email
    identity.nickname = auth.info.nickname
    identity.image = auth.info.image
    identity.phone = auth.info.phone
    identity.urls = (auth.info.urls || "").to_json
    identity.save
    identity
  end
end

Create OmniauthCallbacksController to pull in data

We’re going to build one method to handle the different authentication callbacks, called generic_callback.
The logic of this controller is:
1. Find or create an Identity object for the incoming oauth data. Update it with the latest info.
2. If there is no user associated with the Identity, associate it with the current_user.
3. If there is no current_user, create a new User object.
4. If the User object doesn’t have an email address set yet, but we do have one from the remote service, set the email address to that.
5. Log the user in and let the continue on their way.

rails g controller users/omniauth_callbacks
rails g controller users/registrations

## routes.rb
devise_for :users,:controllers => { omniauth_callbacks: 'users/omniauth_callbacks',registrations: 'users/registrations' }

## app/controllers/users/omniauth_callbacks.rb

class Users::OmniauthCallbacksController <      Devise::OmniauthCallbacksController

  def facebook
    generic_callback( 'facebook' )
  end

  def twitter
    generic_callback( 'twitter' )
  end

  def google_oauth2
    generic_callback( 'google_oauth2' )
  end
  def generic_callback( provider )
    @identity = Identity.find_for_oauth env["omniauth.auth"]
    @user = @identity.user || current_user
    if @user.nil?
      @user = User.from_omniauth(@identity)
      @identity.update_attribute( :user_id, @user.id )
    end
    if @user.email.blank? && @identity.email
      @user.update_attribute( :email, @identity.email)
    end

    if @user.persisted?
      @identity.update_attribute( :user_id, @user.id )
       # This is because we've created the user manually, and Device expects a
       # FormUser class (with the validations)
      @user = User.find @user.id
      sign_in_and_redirect @user, event: :authentication
      set_flash_message(:notice, :success, kind: provider.capitalize) if    is_navigational_format?
    else
      session["devise.#{provider}_data"] = env["omniauth.auth"]
      redirect_to new_user_registration_url
    end
  end
end

Override RegistrationsController to handle adding email address and password

class Users::RegistrationsController < Devise::RegistrationsController
  def update_resource(resource, params)
    if resource.encrypted_password.blank? # || params[:password].blank?
    resource.email = params[:email] if params[:email]
      if !params[:password].blank? && params[:password] ==    params[:password_confirmation]
        logger.info "Updating password"
        resource.password = params[:password]
        resource.save
      end
      if resource.valid?
        resource.update_without_password(params)
      end
   else
     resource.update_with_password(params)
   end
  end
end

Adding methods to User to get to the clients

we will have to add fields of name and username to users table in order to handel twitter authentication as twitter api does not returns user email in the response hash but we get name we will apply some logic to generate an email in case of twitter authentication
rails g migration addColumnNameToUser name:string, username:string
rake db:migrate

i want to implement configrable to devise

rails g migration add_confirmable_to_devise
rake db:migrate

## db/migrate/XXXX.rb

class AddConfirmableToDevise < ActiveRecord::Migration
 def change
  add_column :users, :confirmation_token, :string
  add_column :users, :confirmed_at, :datetime
  add_column :users, :confirmation_sent_at, :datetime
  add_column :users, :unconfirmed_email, :string # Only if using reconfirmable
  add_index :users, :confirmation_token, unique: true
  # User.reset_column_information # Need for some types of updates, but not for     update_all.
  # To avoid a short time window between running the migration and updating all  existing
  # users as confirmed, do the following
  execute("UPDATE users SET confirmed_at = NOW()")
 end
end

##app/models/user.rb

class User < ActiveRecord::Base
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable and :omniauthable
  devise :database_authenticatable, :registerable,:confirmable,
    :recoverable, :rememberable, :trackable, :omniauthable,:validatable,
    :omniauth_providers => [:facebook,:google_oauth2,:twitter]
  has_many :identities
  validates_presence_of :username

  def twitter
    identities.where( :provider => "twitter" ).first
  end

  def twitter_client
    @twitter_client ||= Twitter.client( access_token: twitter.accesstoken )
  end

  def facebook
    identities.where( :provider => "facebook" ).first
  end

  def facebook_client
    @facebook_client ||= Facebook.client( access_token: facebook.accesstoken )
  end

  def google_oauth2
    identities.where( :provider => "google_oauth2" ).first
  end

  def google_oauth2_client
    if !@google_oauth2_client
      @google_oauth2_client = Google::APIClient.new(:application_name => '   App', :application_version => "1.0.0" )
      @google_oauth2_client.authorization.update_token!({:access_token =>   google_oauth2.accesstoken, :refresh_token => google_oauth2.refreshtoken})
    end
    @google_oauth2_client
  end

  def self.from_omniauth(identity)
    user=self.new
    user.name = identity.name # assuming the user model has a name
    user.username = identity.name.gsub(" ","") # assuming the user model has a name
    user.email = identity.email || "#{user.username}-CHANGEME@example.com"
    user.password = Devise.friendly_token[0,20]
    user.skip_confirmation!
    user.save(validate: false)
    return user
  end
end

You can try running this sample app by just cloning it  from https://github.com/shubhangisingh/social_auth.git to you system and add the app id’s and app sectret keys in devise.rb file

Happy Coding …!

Advertisements

One thought on “Setting up User Authentication in Rails (Devise, Facebook,Twitter,Google)

  1. Really nice tutorial, you should update the Devise Initializer Scopes here as well like you have it on github.

    But i do have one problem. It’s currently not possible for a user to update his profile as the current password isn’t matching. So how to change this?

    Like

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s