Rails 4, Devise, Omniauth (with multiple providers)

Mel picture Mel · Jan 21, 2014 · Viewed 11.2k times · Source

I have spent days watching the RailsCasts on devise and omniauth and then going through related tutorials for setting up an authentication system that uses these gems. I think the RailsCasts are out of date and trying to patch the gaps with other tutorials is creating all kinds of issues.

Please can anyone suggest a current tutorial that I can use as a basis for implementing this system. I have separate user and authentications models (with users having many authentications).

I'd really like to use devise and omniauth (with CanCan for abilities) on rails 4 but am tearing my hair out in trying to find a basic setup (using psql as a database).

Answer

Alex Tonkonozhenko picture Alex Tonkonozhenko · Mar 5, 2014

To use Devise with multiple auth providers you need 1 more model - Authorization

#authorization.rb

# == Schema Information
#
# Table name: authorizations
#
#  id           :integer          not null, primary key
#  user_id      :integer
#  provider     :string(255)
#  uid          :string(255)
#  token        :string(255)
#  secret       :string(255)
#  created_at   :datetime
#  updated_at   :datetime
#  profile_page :string(255)
#

class Authorization < ActiveRecord::Base
  belongs_to :user
end

There is code for User model

#user.rb

SOCIALS = {
  facebook: 'Facebook',
  google_oauth2: 'Google',
  linkedin: 'Linkedin'
}


has_many :authorizations

def self.from_omniauth(auth, current_user)
  authorization = Authorization.where(:provider => auth.provider, :uid => auth.uid.to_s, 
                                      :token => auth.credentials.token, 
                                      :secret => auth.credentials.secret).first_or_initialize
  authorization.profile_page = auth.info.urls.first.last unless authorization.persisted?
  if authorization.user.blank?
    user = current_user.nil? ? User.where('email = ?', auth['info']['email']).first : current_user
    if user.blank?
      user = User.new
      user.skip_confirmation!
      user.password = Devise.friendly_token[0, 20]
      user.fetch_details(auth)
      user.save
    end
    authorization.user = user
    authorization.save
  end
  authorization.user
end

def fetch_details(auth)
  self.name = auth.info.name
  self.email = auth.info.email
  self.photo = URI.parse(auth.info.image)
end

And at the end you need to override methods for Devise controller

class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController

  def all
    user = User.from_omniauth(env['omniauth.auth'], current_user)
    if user.persisted?
      sign_in user
      flash[:notice] = t('devise.omniauth_callbacks.success', :kind => User::SOCIALS[params[:action].to_sym])
      if user.sign_in_count == 1
        redirect_to first_login_path
      else
        redirect_to cabinet_path
      end
    else
      session['devise.user_attributes'] = user.attributes
      redirect_to new_user_registration_url
    end
  end

  User::SOCIALS.each do |k, _|
    alias_method k, :all
  end
end

If you need some for providers, e.g. Twitter you need to override twitter method, because it doesn't provide user email and you should save it's credentails in some other way.

Also you should make some changes in routes

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