Multiple Omniauth providers for same User on Ruby on Rails

Recently i've been working on a social platform in Rails and had to implement many Omniauth providers for the same user.

Allowing the user to sign-in to its account using many Omniauth provider. We can also take advantage of the token we get on the callback to post updates to the social network, I'll explain this in a future post.

We'll be working with the following gems:

gem 'devise', '~> 3.2.4'
gem 'omniauth-twitter'
gem 'omniauth-facebook'
gem 'omniauth-google-oauth2'
gem 'fb_graph'

Devise installation and scaffolding

Run bundle install and then rails generate devise:install. Now, let's create the user model by running rails generate devise user. We'll also be needing the devise's views, run rails generate devise:views. To finish, run rake db:migrate. I highly recomend adding username to the user if you are going to use Twitter as a provider. I'll post how to do this later on.

Omniauth Controllers, Models and Configuration

The next step is to create a table that will contain the information of each provider.

Lets start creating a migration for our user providers table:

class CreateUserProviders < ActiveRecord::Migration
  def change
    create_table :user_providers do |t|
      t.references :user, index: true
      t.string :provider
      t.string :uid

      t.timestamps
    end
  end
end

Remember that we are aiming that one user has_many user providers. So let's go and modify the models to acomplish that.

class UserProvider < ActiveRecord::Base
  belongs_to :user
end

class User < ActiveRecord::Base
  has_many :user_provider, :dependent => :destroy
end

You'll have to create 'apps' on the social networks that you want to authenticate with.

I'll work with Facebook, Twitter and Google.

Make sure that you include the :omniauth_providers to devise in the user model:

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

Once you have your APP_ID and APP_SECRET add the following lines to /config/initializers/devise.rb

config.omniauth :twitter, 'APP_ID', 'APP_SECRET'

config.omniauth :facebook, 'APP_ID', 'APP_SECRET' , 
{:scope => 'publish_stream,email,offline_access,manage_pages'}

config.omniauth :google_oauth2, 'APP_ID', 'APP_SECRET'

With that done, let's create the contollers in charge of managing the callbacks to the Omniauth provider.

Add omniauth_callbacks_controller.rb under /app/controllers/users/ and add the following lines:

class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController

end

Since we are using three providers, let's define three functions:

class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController
    def facebook
        @user = UserProvider.find_for_facebook_oauth(request.env["omniauth.auth"])

        if @user.persisted?
          sign_in_and_redirect @user, :event => :authentication               
        else
          session["devise.facebook_data"] = request.env["omniauth.auth"]
          redirect_to new_user_registration_url
        end
    end

    def twitter
        auth = env["omniauth.auth"]

        @user = UserProvider.find_for_twitter_oauth(request.env["omniauth.auth"])
        if @user.persisted?         
          sign_in_and_redirect @user, :event => :authentication
        else
          session["devise.twitter_uid"] = request.env["omniauth.auth"]
          redirect_to new_user_registration_url
        end
    end

    def google_oauth2
        @user = UserProvider.find_for_google_oauth(request.env["omniauth.auth"])
        if @user.persisted?
          sign_in_and_redirect @user, :event => :authentication
        else
          session["devise.user_attributes"] = user.attributes
          redirect_to new_user_registration_url
        end
    end

end

But where are find_for_twitter_oauth, find_for_twitter_oauth and find_for_google_oauth defined you may be asking. Let's head to our UserProvider model and create this functions.

class UserProvider < ActiveRecord::Base
  belongs_to :user


      def self.find_for_facebook_oauth(auth)

        user = UserProvider.where(:provider => auth.provider, :uid => auth.uid).first
        unless user.nil?
            user.user
        else
            registered_user = User.where(:email => auth.info.email).first
            unless registered_user.nil?
                        UserProvider.create!(
                              provider: auth.provider,
                              uid: auth.uid,
                              user_id: registered_user.id
                              )
                registered_user
            else
                user = User.create!(
                    name: auth.info.name,
                            email: auth.info.email,
                            password: Devise.friendly_token[0,20],
                            )
                user_provider = UserProvider.create!(
                    provider:auth.provider,
                            uid:auth.uid,
                              user_id: user.id
                    )
                user
            end
        end
    end

    def self.find_for_google_oauth(auth)
        user = UserProvider.where(:provider => auth.provider, :uid => auth.uid).first
        unless user.nil?
            user.user
        else
            registered_user = User.where(:email => auth.info.email).first
            unless registered_user.nil?
                        UserProvider.create!(
                              provider: auth.provider,
                              uid: auth.uid,
                              user_id: registered_user.id
                              )
                registered_user
            else
                user = User.create!(
                    name: auth.info.name,
                              email: auth.info.email,
                              password: Devise.friendly_token[0,20],
                            )
                user_provider = UserProvider.create!(
                    provider:auth.provider,
                            uid:auth.uid,
                            user_id: user.id
                    )
                user
            end
        end
    end

    def self.find_for_twitter_oauth(auth)
          user = UserProvider.where(:provider => auth.provider, :uid => auth.uid).first
          unless user.nil?
                user.user
          else
                registered_user = User.where(:username => auth.info.nickname).first
              unless registered_user.nil?
                        UserProvider.create!(
                              provider: auth.provider,
                              uid: auth.uid,
                              user_id: registered_user.id
                              )
                   registered_user
              else
                user = User.create!(
                        name: auth.extra.raw_info.name,
                        username: auth.info.nickname,                      
                          password: Devise.friendly_token[0,20],
                          )

                    user_provider = UserProvider.create!(
                        provider:auth.provider,
                          uid:auth.uid,
                          user_id: user.id
                        )
                    user
              end

        end
    end
end

Ok, so what is all this stuff? We have 3 case scenarios in each social platform.

  1. The user has already sign up with that provider.
  2. The user has already sign up with the same email or username the provider is returning, in that case a user provider is created and linked to the user to store the uid.
  3. This is the first time the user is siging up to the site, in this case the user and user provider is created and linked.

As you can see, Twitter doesn't returns email, leaving you with a blank email address. Redirecting the user to edit_user_registration_path after registration to fill the email would be a great idea.

Make sure that you add the omniauth_callbacks on your routes.rb file

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

That's all. You may want to create a settings page where the user can link and unlink social accounts, and allow the user to create passwords in case he wants to log in with his username.

Jorge Caballero

Read more posts by this author.