Last Updated: March 08, 2016
·
4.584K
· jmlevick

Omniauth with multiple providers in a Rails/Mongoid app

NOTES: I'll asume you already have the basics in order: You've installed the gems, You've set omniauth with the keys and secrets for the different providers according to your case, You have routes defined for responding to the different providers callbacks and you have a User model in which to work with a model specific method for creating users from an omniauth hash.

So, the other day I was working on a project where I needed omniauth with multiple providers in a rails app which used Mongoid as ORM. It wasn't very difficult, but certainly had it's paritcularities, let's see:

First of all, we have to set some config to our "Mongoid.yml" file, this step it's easier to explain if I post an image:

Picture

As you can see, we need a "direct child" options hash for our enviroment, setting the "raise not found error" option to false, as we need to get a nil if no records are found instead of an exception.

Then, we need to have a users model with a referenced identities model. In my setup I was using Devise, so the User model was automatically created and I just had to create the Identities one.

The models (trimmed):

User Model

class User
  include Mongoid::Document
  has_many :identities, :dependent => :destroy
...
end

Identity Model

class Identity
  include Mongoid::Document
  belongs_to :user
  field :uid, type: String
  field :provider, type: String

  index({ uid: 1}, {drop_dups: false, background: true})

end

As you can see, I've indexed the identities a user can have because the way this works is searching through our database for an existing identity in order to apply different conditionals as we'll see later... (Indexing them makes this fast as hell haha); It's important to prevent dropping dups because the logic we need to implement this will first create an identity with no "user_id" if the identity the user is using to sign in has never been persisted to our database; Later we'll remove duplication, keep reading.

After we are done with this, we have to modify our controller which responds to the different omniauth callbacks, mine as follows:

class OmniauthCallbacksController < Devise::OmniauthCallbacksController
  skip_before_filter :verify_authenticity_token
  def all
    auth = request.env["omniauth.auth"]
    # Find an identity here
    @identity = Identity.all.find_by(:uid => auth.uid, :provider => auth.provider)

    if @identity.nil?
    # If no identity was found, create a brand new one here
      @identity = Identity.create(:uid => auth.uid, :provider => auth.provider)
    end
    if signed_in?
      if @identity.user == current_user
        # User is signed in so they are trying to link an identity with their
        # account. But we found the identity and the user associated with it 
        # is the current user. So the identity is already associated with 
        # this user. So let's display an error message.
        redirect_to user_url, notice: "Already linked that account!"
      else
        # The identity is not associated with the current_user so lets 
        # associate the identity
        @identity.user = current_user
        @identity.save()
        redirect_to user_url, notice: "Successfully linked that account!"
      end
    else
      if @identity.user.present?
        # The identity we found had a user associated with it so let's 
        # just log them in here
        user = @identity.user
        flash.notice = "Signed in!"
        sign_in_and_redirect user
      else
        # Logic for the case when we actually need to create a new user
          user = User.from_omniauth(auth)
          if user.persisted?
            flash.notice = "Signed in!"
            sign_in_and_redirect user
          else
            session["devise.user_attributes"] = user.attributes
            redirect_to new_user_registration_url
          end
        end
      end
    end
  alias_method :facebook, :all
  alias_method :twitter, :all
  alias_method :google_oauth2, :all 
end

Basically you can use the same controller as mine, (skipping the devise inheritance if you are not using Devise) and replacing the Logic for the case when we actually need to create a new user part for your own depending on your case. What this part should do is creating a user from the omniauth hash, I've added some extra logic for my devise setup but it's not really important actually. Other thing to be aware of is that you have to setup your own action according to the enviroment of your app in case the identity we find has a user (user.present?), In my controller I'm using the devise's specific "sign in and redirect user" method. Notice the aliases for the all method supplying a "diferent action" (but actually returning the same method) for the different providers we wanna support and the order of the arguments in the find_by and create! actions wich obeys the structure of our identity model and the way the index has been created, this is for efficient index use. Finally, let's take a look at my User.from_omniauth method:

def self.from_omniauth(auth)
  where(auth.slice(:uid, :provider)).first_or_create do |user|
    user.uid = auth.uid
    user.provider = auth.provider
    user.name = auth.info.name
    user.email = auth.info.email
    user.image = auth.info.image
  end
end

This method is only triggered when we are actually creating a new user from an omniauth hash.

One more thing... Do you remember the identity duplication we discussed earlier? It can be solved with a simple callback in our user model, wich gets triggered after users creation:

after_create :first_identity

def first_identity
  self.identities.first_or_create!(:uid => self.uid, :provider => self.provider, :user_id => self.id)
  Identity.where(:user_id => nil).destroy_all
end

And... basically that's it. I think I didn't forget anything. There you are, instructions for implementing omniauth with multiple providers in your Mongoid/Rails app.

P.S. If you're getting "Invalid Token" errors when doing some POST requests via a form of something ins your app as a recently created user, add this line to the head section of your layout:

<%= csrf_meta_tag %> 

Right under the <%= csrfmetatags %> one.

C'ya! ;)

5 Responses
Add your response

Not sure, but is this volitonal?

user.uid = auth.uid
user.provider = auth.provider

user.uid and user.provider is not a User method. They should be Idendity methods, or I am wrong?

This is the part I am hanging.

over 1 year ago ·

You require them in both user and identity as "fields" because of the relation. You need to check if that user has already that identity linked or not and if that identity actually belongs to that user or not.

over 1 year ago ·

Ok, I agree with that. But what when a user registers without an "omniauth". I mean a "regular" user which registers via username & password?

firstidentity gets called :aftercreate and the self.uid, self.provider, self.id and self.token will be blank at this time?

over 1 year ago ·

i added =>
def self.fromomniauth(auth)
...
end
to my user model but i have this error!!??
undefined method `from
omniauth' for User:Class

over 1 year ago ·

Would it not be better to rescue from Mongoid::DocumentNotFoundError instead of completely disabling it ?

over 1 year ago ·