Pregunta

I am trying to create a rails 4 app with omniauth devise :

  • signin with facebook
  • signin with google
  • signin with linkedin
  • signin with twitter

Here, I am able to login in with either facebook, linkedin, twitter or google account. But my problem is: my google account email and linkedin email address are same. And login with google and then login with linkedin is giving me this error:

"Validation failed: Email has already been taken"

This is a problem because devise uses :unique => true in migration file for email field.

Can anyone provide me nice idea to handle this error please?

¿Fue útil?

Solución 2

I followed these steps and working fine for me:

1. Gemfile

gem 'omniauth-facebook', '1.4.0'
gem 'omniauth-twitter'
gem 'omniauth-google-oauth2'

2. config/route.rb

devise_for :users, controllers: { omniauth_callbacks: "omniauth_callbacks" }

3. Links

<%= link_to "Sign in with Facebook", user_omniauth_authorize_path(:facebook) %>
<%= link_to "Sign in with twitter", user_omniauth_authorize_path(:twitter) %>
<%= link_to "Sign in with google", user_omniauth_authorize_path(:google_oauth2) %>

4. controllers/omniauth_callbacks_controller.rb

class OmniauthCallbacksController < Devise::OmniauthCallbacksController
  skip_before_filter :authenticate_user!

  def all
    user = User.from_omniauth(request.env["omniauth.auth"], current_user)
    if user.persisted?
            flash[:notice] = "you are successfully logged in!!"
            sign_in_and_redirect(user)
        else
            session["devise.user_attributes"] = user.attributes
            redirect_to new_user_registration_url
        end
  end

  def failure
    super
  end

  alias_method :facebook, :all
  alias_method :twitter, :all
  alias_method :google_oauth2, :all
end

5. add required fields and model

rails g migration add_social_network_info_columns_to_users name image_url locations

# generate new model Authorization
rails g model Authorization user_id:integer provider uid token secret username

6. models/User.rb

class User < ActiveRecord::Base


require 'securerandom'

  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable and :omniauthable
  devise :database_authenticatable, :registerable,
    :recoverable, :rememberable, :trackable, :validatable,
    :omniauthable

  has_many :authorizations

  # omniauth facebook provider
  def self.from_omniauth(auth, current_user)
    # check for existing authorization
    # Find or create Authorization with: provider, uid, token and secret
    authorization = Authorization.where(
      :provider => auth.provider, 
      :uid => auth.uid.to_s, 
      :token => auth.credentials.token, 
      :secret => auth.credentials.secret
    ).first_or_initialize

    if authorization.user.blank?
      user = current_user.nil? ? User.where('email = ?', auth["info"]["email"]).first : current_user

      # save user related data in user table
      if user.blank?
        User.new(
          :email            => auth.info.email,
          :password         => Devise.friendly_token[0,10],
          :name             => auth.info.name,
          :locations        => auth.info.location,
          :image_url        => auth.info.image
        )
        # since twitter don't provide email, 
        # so you need to skip validation for twitter.
        auth.provider == "twitter" ?  user.save!(:validate => false) :  user.save!
      end

      # store authorization related data in authorization table
      authorization.username = auth.info.nickname
      authorization.user_id = user.id
      authorization.save!
    end
    authorization.user
  end
end

6. model/Authorization.rb

class Authorization < ActiveRecord::Base
  belongs_to :user
end

source: https://github.com/mohitjain/social-login-in-rails

Otros consejos

I think that basically, if handling all the cases properly, this can be really complex unless you choose option 1 below (and even then, there are issues to consider which I outline afterwards). Sorry for the length of this answer!

I'm assuming you've done something like this:

https://github.com/plataformatec/devise/wiki/OmniAuth:-Overview

so far, which gets you some of the way but doesn't handle the problem you're encountering.

The way I have done this, I have a User who has_many Identities. Identity stores the name of the external service, the user id it tells you and whatever else you want. This means that the same user can log in with multiple identities (twitter, Facebook…). Have you seen:

http://railscasts.com/episodes/235-omniauth-part-1?view=asciicast

http://railscasts.com/episodes/236-omniauth-part-2?view=asciicast

which help with getting going with the User has_many Identities, but still don't deal with your case.

Then to solve your issue, one option is to detect the validation error you're encountering:

if @user.errors.added?(:email, :taken)
  # do whatever you want - e.g. one of the 4 options below.
end

and if it occurs, you could either:

  1. just add the identity to identities associated with that existing user who has the same email address and then sign them in.

    or

  2. before adding the identity to the existing user, ask for the password for the existing user account (if the account was originally registered via devise on your system), so you'll need to go to some new controller/action and view that handles this flow.

    or

  3. Perhaps send an email confirmation (not devise's standard confirmation) to acknowledge that they are linking their new identity to an existing account. This sounds a bit complicated, because you'll have to store the identity temporarily somewhere (probably in a database if you want to cope with them ending their current session before clicking a confirmation link), flagging it as unconfirmed, until they click a confirmation link in an email (which you'll also have to deal with generating).

    or

  4. Perhaps force them to authenticate with the other identity that has the same email address. This has the advantage over the previous option that you can just save the new identity info in the session and get them to authenticate using the other service immediately, but obviously there will be some work to handle the flow there.

Option 1 can be less secure because you are trusting that external services have confirmed the user's email address - which perhaps they have - but you want to be sure otherwise someone could sign up with linkedin using their email address and then sign in to your site and then an attacker could sign up with another service but using the same email address. They could then access the person's account on your site if you didn't confirm they really owned the email address somehow (e.g. by using option 2, 3 or 4). If the external services can confirm they've verified the email addresses, then this option should be ok and is by far the simplest - for example, Facebook have a field that tells you the account has been verified (but see my comments below about services that don't need email addresses). If they're merging with an account registered with you directly (doesn't sound like your situation), you should have confirmed the email address they registered using devise's standard Confirmable feature.

Option 2 sounds like it doesn't apply in your case, because you don't mention that a user can register with you directly via devise; only sign in using external services. This means that they have no password on your site that they know. (You've probably added a dummy one to get the devise validation to pass, unless you've disabled that, but they won't know the password unless you've told them somehow, and it could be confusing to them to do that).

Option 3 sounds do-able, though I haven't tried it. It's a bit more laborious for the user.

Option 4 also sounds do-able, though I haven't tried it either.

So far, I've done option 2 because users can only register with my site either directly using devise or via 1 external service. I'll be adding other services soon, so I plan to use option 4 (perhaps only if external service says they haven't confirmed the email address, and option 1 otherwise).

Options 2, 3 and 4 are a fair bit more work than option 1, so it depends if you can confirm that the external services have verified the email addresses and if not, how paranoid you are about attackers being able to access user accounts on your site. Personally, I err on the side of paranoia!

This might also give you some more useful info:

https://github.com/intridea/omniauth/wiki/Managing-Multiple-Providers

but because omniauth itself doesn't concern itself with model issues, it mostly sidesteps it, though it says for your case that it is "probably sufficiently prudent to assume that they are, in fact, the same person who also created the previous user" but you have to be able to trust the external services as I mentioned above.

There are also other things to consider, such as the case where someone has the same email address registered with Facebook and linked in and has signed in with both on your site (so single user account once you've dealt with your issue) and then changes the email associated with their Facebook account but not linkedin. If you always overwrite the email stored in the user table with the one from the external service, then it'll keep changing if they log in with linkedin and then Facebook (but maybe this doesn't matter to you). Alternatively, they may have different email addresses registered with Facebook and linked in and have logged in with both on your site (so 2 different users on your site) and then they change their linked in email address to be the same as the Facebook one. If you update the email address for a user every time they log in via an external service, you'll have your "Email already taken" error, but in this case you have 2 existing users to merge which could be interesting depending on what else in your database is associated with a user...

Also, I don't think twitter returns an email address, so if the same person has logged in with twitter and linkedin, you won't detect this. Furthermore, I think email is optional with Facebook (you can use a mobile phone number), so the same thing can happen with Facebook. My ideal solution would allow the user to merge arbitrary accounts, obviously having to enter whatever credentials are required to confirm they own the accounts they are merging! I haven't done this yet, but it's on my wish list!

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top