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:
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! ;)
Written by José Manuel Escudero
Related protips
5 Responses
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.
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.
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?
i added =>
def self.fromomniauth(auth)
...
end
to my user model but i have this error!!??
undefined method `fromomniauth' for User:Class
Would it not be better to rescue from Mongoid::DocumentNotFoundError
instead of completely disabling it ?