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?
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:
just add the identity to identities associated with that existing user who has the same email address and then sign them in.
or
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
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
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!
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
Related
I have Devise setup to allow login with email or username. With your username you can have a vanity URL like so: vanity.com/username. My User model thus has attr_accessible :username as well as attr_accessor :login.
To prevent usernames from colliding with future features, I want to implement a blacklist on certain usernames. You can see a nice example list in use by GitHub here.
I'm new to Devise and have searched the how-to's in their wiki to see if this use case or anything like it is covered there. It doesn't seem to be.
How can I blacklist certain usernames for registration in Devise?
There was an easier way to do this than having to modify Devise's controllers.
In the User model all I had to do was:
validates :username, :exclusion => %w(about blog ...)
Way too simple.
I'm using Devise 2.1.2 with multiple OmniAuth providers. My devise.rb file contains this line:
config.omniauth :linkedin, API_KEY, SECRET_KEY, :scope => 'r_emailaddress', :fields => ["email-address"]
It is currently stripped down to just email-address since that is the only thing acting strange. Taking a look inside request.env['omniauth.auth'].info, the email key is blank.
How come? I don't want to bypass validation, I wan't to use the email address from the users LinkedIn account.
Thanks to the thread link in Remus Rusanu's answer, I noticed this post by a LinkedIn employee:
Hey guys, we're working on a migration plan for existing applications. The new member permissions only apply to newly registered applications.
Thanks!
Kamyar
Due to no patience, creating a new LinkedIn application allowed me to retrieve the user email address. How nice that they finally changed their mind about this.
Given that LinkedIn as a matter of policy does not share the email over oauth, I'm not at all surprised.
I am using omniauth with LinkedIn as a provider. LinkedIn doesn't supply
an email in info hash, so i cannot provide an email when create the user based
on the information I get back.
Two related questions:
1) How can I adjust devise so that there isn't a requirement
for :email as a validation? It doesn't appear to be set under the
User model.
2) I do want to get the email information, however, so want to have
email information requested before creating the User. How can I
redirect to a page/wizard asking for email information and then come
back to finish the user registration?
I just solved this without needing to use the separate 'linkedin' gem, it was pretty difficult as there was a distinct lack of documentation on the subject!
Firstly you need to make the email-address available by adding the fields option to your LinkedIn Omniauth configuration, you will also need to override the request_token_path in order to add the r_emailaddress scope required to retrieve a users email address.
Mine ended up looking something like this (NB. Ruby 1.9.3):
provider :linkedin, external_services['linkedin']['api_key'], external_services['linkedin']['api_secret'], client_options: {request_token_path: '/uas/oauth/requestToken?scope=r_emailaddress'}, fields: ['id', 'first-name', 'last-name', 'headline', 'industry', 'picture-url', 'public-profile-url', 'email-address']
NOTE: Dont forget to change external_services['linkedin']['api_key'] and external_services['linkedin']['api_secret'] to your own.
Your user will then be asked to authorise use of their email address as well as their basic provide and you will have access to it once they are returned via:
auth['extra']['raw_info']['emailAddress']
I should probably commit this change back to omniauth-linkedin so you can simply set scope: r_emailaddress in the provider options, avoid the duplication of the field names and get the email back in the info section of the auth object.
If I get time after this section of my project is finished I will.
Take a look at the railscasts about omniauth: http://railscasts.com/episodes?utf8=%E2%9C%93&search=omniauth
The idea is the following:
Create a new user from the omniauth info
try to save the user
since the email is not present it won't validate
store the omniauth data in the session and redirect_to new_user_registration_url
create your own registration controller that inheritates from the devise one
override the build_resource(*args) method, and if the omniauth data is present, use it to create the resource (User in your case)
That way, after trying to login with linkedin, the user will be redirected to a form where he will be able to enter his email.
It's all explained in the railscast ;)
Using Rails 3.0.6, Omniauth 0.2.0 and Devise 1.2.1, I'm encountering the following situation:
I want to offer users the option to authenticate via Facebook. I have a user system set up using Devise and I can successfully auth using Facebook. I've spent several hours trying to code the behavior I want for one specific situation:
user is not logged in
user has a site account
user authenticates via Facebook
I offer the user 2 choices at this point
create an account (can be a dummy account with no provided info)
link this Facebook authentication with an existing account
I'm having trouble with the latter option. The user has already authenticated but I still need him to log in with his site account. I have an action in my AuthenticationsController that will associate this authentication with a logged in user. Devise doesn't seem to offer a way for me to log the user in while staying in the same action, though. This was my first attempt to do this
class AuthenticationsController < ApplicationController
before_filter :authenticate_user!, :only => :auth_link_existing_user
...
def auth_link_existing_user
...
end
However, using this method, if the user logs in, they're simply redirected to my site's root page. I know I can change Devise's sign-in redirect, but that will be for all sign-ins. I wanted only this situation to have a separate redirect.
After reading through this mailing list question, I tried to extend SessionsController with my own custom behavior:
def create
resource = warden.authenticate!(:scope => resource_name, :recall => "#{controller_path}#new")
set_flash_message(:notice, :signed_in) if is_navigational_format?
sign_in(resource_name, resource)
if params[:redirect] #new
redirect_to params[:redirect].to_sym #new
else
respond_with resource, :location => redirect_location(resource_name, resource)
end
end
This doesn't work either. I've defined my auth_link_existing_user route to use a POST verb (which seems accurate) and redirects can only be GETs.
So now I do have a solution in mind: copy and paste code from Devise's authenticate_user! helper into a new function which can be called within a controller action without redirecting. This seems less than ideal to me because it's duplication of code and increases coupling--a Devise or Warden update that changes this behavior will break my code as well.
Has anyone else tried something like this and come up with a more elegant solution? Do you see a simpler way for me to offer this or similar behavior to my users?
UPDATE: For anyone who wants to use my dirty solution at the end, this is what I did:
def auth_link_existing_user
# FROM Devise/sessions/create
resource = warden.authenticate!(:scope => :user, :recall => "registrations#auth_new")
set_flash_message(:notice, :signed_in) if is_navigational_format?
sign_in(:user, resource)
# method defined in Ryan Bates' Railscast for Omniauth w/Devise
current_user.apply_omniauth(session[:omniauth])
current_user.save
end
note that this action MUST be placed in your sessions controller. If not, Warden will give you an "invalid email/password" error. It was an incredibly long debugging process to find the source.
With this in place, I use a login form to submit to this action after the user has authenticated.
I like how clean your solution is, though it goes deeper into the stack.
Here is how I've implemented something similar by following the Devise+Omniauth Facebook example on the Devise wiki and modifying the facebook method to pass on the session information to the Login form, with something like this:
if #user.persisted?
flash[:notice] = I18n.t "devise.omniauth_callbacks.success", :kind => "Facebook"
sign_in_and_redirect #user, :event => :authentication
else
session["devise.facebook_data"] = request.env["omniauth.auth"]
redirect_to new_user_session_url
end
Then, in your case, I'd check in Login controller action for session["devise.facebook_data"], submit the uid + token with the form and apply_omniauth if present.
I'm creating a new Rails 3 app and I want to allow users to sign-in using their Facebook or Twitter credentials.
I don't know whether I should implement this using Devise and OmniAuth, or just OmniAuth. I just watched Ryan Bate's screencast on Simple OmniAuth and it seems like I could just use OmniAuth, but I'm not sure it's enough.
I have the following requirements:
Allow sign-in via Facebook and/or Twitter. I will not be implementing local user accounts/passwords.
Signing in via FB/Twitter for the first time should create a new user in the db so I can store the associated FB/Twitter oAuth tokens.
Users should be able to associate both a FB and a Twitter acct to their profile/user so they can post to both FB and Twitter.
Users should be able to delete their account.
I posted this on the Devise Google Mailing List and got this response from José Valim (Devise maintainer and Rails core team member):
"You can use just OmniAuth. If you use Devise, the only benefit is that it will add Omniauth url helpers, but that is so minimal that honestly is not worth the overhead."
-- José Valim
Devise is a fancy way to automatically handle all the things that go with user accounts. If you don't need all the bells and whistles, you should definitely just go the Simple OmniAuth way like in the screencast.
The only hitch I see with trying to link up a Facebook and Twitter account is that you'll have to require them to be signed into one in order to link the other -- and if they do happen to sign in on separate occasions, you could possibly have two Users in your database. This wouldn't be a problem if you were doing Google and Facebook because they both send back an email address, but Twitter only sends back a username, no email address.
You'll have to add a field to the User model for a username (Twitter) and email address (Facebook) so you can attempt to link the accounts if a visitor did it separately and wants to link them later. Just be careful of that when you set it up.
If you can't use just OmniAuth, maybe because, like in my case, you want to use ActiveAdmin which depends on Devise and methods like current_user would conflict, you can just override the login page with your own:
match '/users/sign_in', :to => "sessions#new"
devise_for :users, :controllers => { :omniauth_callbacks => "users/omniauth_callbacks" }
I have a "Sign in with Facebook" link in my nav, but this page is necessary for redirecting users when they're trying to access a protected page while not logged in.
Edit: Actually, there is a section on the wiki page: "Using OmniAuth without other authentications"