Rails 4 with Devise, Omniauth and multiple service providers - ruby-on-rails

I am trying desperately to find a way to get devise to work in my Rails 4 app. I have tried every tutorial I can find to get this set up.
The current tutorial is:http://sourcey.com/rails-4-omniauth-using-devise-with-twitter-facebook-and-linkedin/
My previous bounty questions on related problems (commonly voted down & I don't understand why), show my other attempts at getting this done.
My problem currently is with this bit of text in the user model:
def self.find_for_oauth(auth, signed_in_resource = nil)
# Get the identity and user if they exist
identity = Identity.find_for_oauth(auth)
# If a signed_in_resource is provided it always overrides the existing user
# to prevent the identity being locked with accidentally created accounts.
# Note that this may leave zombie accounts (with no associated identity) which
# can be cleaned up at a later date.
user = signed_in_resource ? signed_in_resource : identity.user
# Create the user if needed
if user.nil?
# Get the existing user by email if the provider gives us a verified email.
# If no verified email was provided we assign a temporary email and ask the
# user to verify it on the next step via UsersController.finish_signup
email_is_verified = auth.info.email && (auth.info.verified || auth.info.verified_email)
email = auth.info.email if email_is_verified
user = User.where(:email => email).first if email
# Create the user if it's a new registration
if user.nil?
user = User.new(
name: auth.extra.raw_info.name,
#username: auth.info.nickname || auth.uid,
email: email ? email : "#{auth.uid}-#{auth.provider}.com",
password: Devise.friendly_token[0,20]
)
user.skip_confirmation!
user.save!
end
end
The problem is (pointing at this line: user = User.new) in the above method:
unknown attribute 'name' for User.
In the previous tutorial I tried, I discovered that linkedin worked when i changed the attributes to:
# self.email = omniauth['extra']['raw_info']['emailAddress']
# self.first_name = omniauth['extra']['raw_info']['firstName']
# self.last_name = omniauth['extra']['raw_info']['lastName']
The field names for the omniauth strategy are different to what's shown in the tutorial (at least for linkedin). But then if Facebook or twitter use different field names again, how does the tutorial solve for that?
Also, in my user table, I have attributes called first_name and last_name, but I tried changing the line to:
first_name: auth.extra.raw_info.'firstName',
That didn't work either.
When I try:
first_name: auth.extra.raw_info.name
I get this error:
undefined method `password_required?' for #<User:0x007fb7bc2ce6f8>
But anyway, I only want first name in that field and i think this is putting the whole name into first name (although Im not sure about that). Also, if this is going to be amended to work for linkedin, will that mean it will not work for Facebook and twitter?
It's all a big mess. I'm growing increasingly frustrated with this. Does anyone know how to solve this particular problem. I have been trying for 2.5 years to get devise/omniauth working.
My recent previous tutorial linked questions are:
https://stackoverflow.com/questions/33888972/rails-devise-omniauth-strategies-omniauthcallbackscontroller
Devise Omniauth - setup & defining strategies
Rails, Devise & Omniauth - problems with setup
There are several others, but I'm not figuring this out by my own efforts. I've had a few sessions on codementor.io but not been able to find help. A source of help would be greatly appreciated.
So trying the suggestion below, I tried changing the method in the user model to:
def self.find_for_oauth(auth, signed_in_resource = nil)
# Get the identity and user if they exist
identity = Identity.find_for_oauth(auth)
# If a signed_in_resource is provided it always overrides the existing user
# to prevent the identity being locked with accidentally created accounts.
# Note that this may leave zombie accounts (with no associated identity) which
# can be cleaned up at a later date.
user = signed_in_resource ? signed_in_resource : identity.user
# Create the user if needed
if user.nil?
# Get the existing user by email if the provider gives us a verified email.
# If no verified email was provided we assign a temporary email and ask the
# user to verify it on the next step via UsersController.finish_signup
email_is_verified = auth.info.email && (auth.info.verified || auth.info.verified_email)
email = auth.info.email if email_is_verified
user = User.where(:email => email).first if email
# Create the user if it's a new registration
if user.nil?
user = User.new(
case auth.provider
when 'linkedin'
first_name: auth.extra.raw_info.firstName,
last_name: auth.extra.raw_info.lastName,
email: emailAddress ? email : "#{auth.uid}-#{auth.provider}.com",
#username: auth.info.nickname || auth.uid,
# email: email ? email : "#{auth.uid}-#{auth.provider}.com",
password: Devise.friendly_token[0,20]
when 'facebook'
first_name: auth.extra.raw_info.first_name,
last_name: auth.extra.raw_info.last_name,
email: auth.extra.raw_info.email ? email : "#{auth.uid}-#{auth.provider}.com",
password: Devise.friendly_token[0,20]
when 'twitter'
first_name: auth.extra.raw_info.nickname,
end
)
user.skip_confirmation!
user.save!
end
end
There are several problems with this, being:
unexpected ':', expecting keyword_end first_name: auth.extra.raw_info.firstName,
unexpected tLABEL, expecting '=' last_name: auth.extra.raw_info.lastName
unexpected tLABEL, expecting '=' email: emailAddress ? email : "#{au... ^
unexpected ',', expecting keyword_end
unexpected keyword_when, expecting keyword_end when 'facebook' ^
unexpected ':', expecting keyword_end first_name: auth.extra.raw_info.first_name, ^
unexpected tLABEL, expecting '=' last_name: auth.extra.raw_info.last_name,
unexpected tLABEL, expecting '=' email: auth.extra.raw_info.email ? ... ^
unexpected ',', expecting keyword_end
unexpected keyword_when, expecting keyword_end when 'twitter' ^
unexpected ':', expecting keyword_end first_name: auth.extra.raw_info.nickname, ^
unexpected keyword_end, expecting '='
I don't know how to solve any of this - I can't find any example (other than the one in the post below) of how to approach this - and that's clearly not working).
On top of all of the above, what do I do with Twitter? It has a nickname field. How can I separate the words out so the first word is saved as first_name and the 2nd word is saved as last_name?
Extrapolating from the particularly snarky comments below, I tried that suggestion again, with if statements. It still doesnt work.
def self.find_for_oauth(auth, signed_in_resource = nil)
# Get the identity and user if they exist
identity = Identity.find_for_oauth(auth)
# If a signed_in_resource is provided it always overrides the existing user
# to prevent the identity being locked with accidentally created accounts.
# Note that this may leave zombie accounts (with no associated identity) which
# can be cleaned up at a later date.
user = signed_in_resource ? signed_in_resource : identity.user
# Create the user if needed
if user.nil?
# Get the existing user by email if the provider gives us a verified email.
# If no verified email was provided we assign a temporary email and ask the
# user to verify it on the next step via UsersController.finish_signup
email_is_verified = auth.info.email && (auth.info.verified || auth.info.verified_email)
email = auth.info.email if email_is_verified
user = User.where(:email => email).first if email
# Create the user if it's a new registration
if user.nil?
user = User.new(
if auth.provider = linkedin
first_name: auth.extra.raw_info.firstName,
last_name: auth.extra.raw_info.lastName,
email: emailAddress ? email : "#{auth.uid}-#{auth.provider}.com",
#username: auth.info.nickname || auth.uid,
# email: email ? email : "#{auth.uid}-#{auth.provider}.com",
password: Devise.friendly_token[0,20]
end
if auth.provider = facebook
first_name: auth.extra.raw_info.first_name,
last_name: auth.extra.raw_info.last_name,
email: auth.extra.raw_info.email ? email : "#{auth.uid}-#{auth.provider}.com",
password: Devise.friendly_token[0,20]
end
if auth.provider = twitter
first_name: auth.extra.raw_info.firstName,
end
)
user.skip_confirmation!
user.save!
end
end
Absent finding a solution to this on this board, I would appreciate advice on how much you think it would be reasonable to pay a professional to help resolve these problems.
I HAVE GONE RIGHT BACK AND COMMENTED OUT ALL OF THE CODE RELATED TO DEVISE AND OMNIAUTH AND NOW TRYING AGAIN, WITH THE DOC ON THE OMNIAUTH WIKI CALLED: MANAGING MULTIPLE PROVIDERS
It seems this doc may have typos in it that experienced coders can read past.
Currently, there is an error message being generated as follows:
/Users/config/routes.rb:35: syntax error, unexpected [, expecting keyword_do or '{' or '(' ...', to: 'sessions#create', via [:get, :post] ... ^ /Users/config/routes.rb:36: syntax error, unexpected [, expecting keyword_do or '{' or '(' match '/logout', to: 'sessions#destroy', via [:get, :post] ^
I have copied these routes directly from the user guide. I'm by far from an expert but I'm also confused about why '==' is used in some places and '=' is used in others in this doc. For example:
if signed_in?
if #identity.user == current_user
Whilst:
#identity.user = current_user
In the same method there is a variance.
I'm also confused about why the sessions controller doesnt inherit from the devise controller. In each of the other tutorials I have done that have had a sessions controller, it has inherited from devise.
There are quite a few other confusing aspects of this tutorial (like why doesnt it have a registrations controller, why are there no other routes for devise, why does the application controller have create and destroy methods)?
Desperately seeking help.

This is not a complete answer, but I have solved part of my problem.
The point of the oauth gem is to take the attributes from each social media strategy and unify them into a common form of expression.
By incorporating raw_info from the strategy, the code is not working with oauth. For example, linkedin auth hash returns data labelled 'firstName' where oauth recognises that as first_name. If you use
first_name: auth.extra.raw_info.first_name,
the attribute will be nil when linked is being called. This is already strange to me since LinkedIn gives basic profile details which suggest a label is first-name.
Anyway, the part I fixed is to remove reference to 'extra.raw' in the above line and use auth.info.first_name. That should resolve differences between strategy labels.
I am still working on the rest of the problems arising in this set up. Some of the issues in the sourcey tutorial are syntax, others are more substantial. I'll post again if I can sort them out.

unknown attribute 'name' for User.
This means that there's no name column in users table. You need to create this column or use some other column that you have (like first_name or username). Like so:
username: auth.extra.raw_info.name
But then if Facebook or twitter use different field names again, how
does the tutorial solve for that?
Yeah, this tutorial gives only one variant, but others are not so difficult to find out. You just need to know, what fields returns this or that provider (twitter, facebook or linkedin) and do something like this:
def self.find_for_oauth(auth, signed_in_resource = nil)
...
# Create the user if needed
if user.nil?
case auth.provider
when 'facebook'
# Take fields that Facebook provides and use them when creating a new user
when 'twitter'
# Take fields that Twitter provides and use them when creating a new user
when 'linkedin'
# Take fields that Linkedin provides and use them when creating a new user
end
end
...
end
It will be better for code organization if you implement separate methods for each provider and call them in certain when.
Also check this out https://github.com/intridea/omniauth/wiki/Auth-Hash-Schema to understand what's inside env['omniauth.auth'].

Related

get first_name and last_name fields from facebook omniauth

I am now implementing omniauth feature into my app. Everything works fine except that i cant get the first and last name from the facebook. Here is my model code.
def self.from_omniauth(auth)
user = User.where(email: auth.info.email).first
if user
return user
else
where(provider: auth.provider, uid: auth.uid).first_or_create do |user|
user.provider = auth.provider
user.uid = auth.uid
user.first_name = auth.info.first_name
user.last_name = auth.info.last_name
user.email = auth.info.email
user.image = auth.info.image
user.password = Devise.friendly_token[0,20]
end
end
I already have the strong parameters setup properly for devise as am using that now for default authentication and is working properly.Is there any additional permissions necessary for first and last name from facebook?
After some fiddling around i found the solution. Now i think we have to explicitly require the fields we require. For me the fix is just to add first_name and last_name to facebook.
In my initializers i added first_name and last_name to info fields.
info_fields: 'email, first_name, last_name'
Update
My full config file will look like this now
config.omniauth :facebook, ENV["FACEBOOK_APP_ID"], ENV["FACEBOOK_SECRET"], scope: 'email', info_fields: 'email, first_name, last_name'
From checking the Facebook docs, the first_name and last_name fields are both part of the public profile so your permissions should be fine.
No idea if this would work (in fact, I would kind of hope it doesn't), but in the implementation we've got working in production at the moment we're using Hash accessors instead of methods. So:
new(
first_name: oauth_data["info"]["first_name"],
last_name: oauth_data["info"]["last_name"],
...
Given your email etc fields are getting set correctly, I'd be surprised if trying that works, but it might be worth a shot.
Failing that, have you got any validations or before_create callbacks which could be interfering somehow?
Ran into this issue while designing the OAuth flow for an app that was using Devise + omniauth-facebook.
Using the public_profile scope, you get back a name attribute on the auth hash.
request.env["omniauth.auth"]
My issue is that a user has one profile and it is autosaved ( user is required to enter a first name and last name at sign up which is validated on the profile model)
Solution: Create a name parsing method in your omniauth services model, which you can pass the request.env["omniauth.auth"]["info"]["name"] as an argument.
# auth hash returns a name like "John Doe Smith"
def parse_name_from_string(ful_name_string)
name_array = name_string.split(" ") # break apart the name
# I chose to return a PORO so i can do something like parsed_user_name_from_facebook_auth.first_name
{
last_name: name_array.pop,
first_name: name_array.join(" ")
}
end

Devise + Omniauth-Facebook get user's phone #

I'm using Devise for authentication in my Rails 3.2 app and am having trouble configuring omniauth-facebook to get the new user's phone number.
First of all: I'm not even sure that it's possible to get a phone number, so if that's the case and someone knows for sure, I'd be happy just for a confirmation.
It doesn't appear from https://github.com/mkdynamic/omniauth-facebook that "phone" is part of the FB auth hash by default, though the general Omniauth schema does have .info.phone (not required, of course). So my first idea was that it's a FB permissions problem. I'm not sure what permissions to use, though, as the Permissions with Facebook Login page doesn't say where to find the phone value (maybe this means it's just not possible?).
I have phone as a required attribute on the User model, so when I try to get it through FB the new object never persists. It works fine without looking for a phone number.
My config file:
# /config/initializers/devise.rb
config.omniauth :facebook, 'FACEBOOK_APP_ID', 'FACEBOOK_APP_SECRET', scope: 'email,public_profile', display: 'page'
In my user model:
# user.rb
def self.from_omniauth(auth)
where(auth.slice(:provider, :uid)).first_or_create do |user|
user.email = auth.info.email
user.password = Devise.friendly_token[0,20]
user.first_name = auth.info.first_name
user.last_name = auth.info.last_name
user.phone = auth.extra.raw_info.phone # have also tried auth.info.phone
end
end
Thanks in advance for any help you may be able to provide!
There's currently no way to get a user's phone number from Facebook.
Facebook's Graph API reference lists all the user information that you can access via the API. A user's phone number is not on the list.
Also, to inspect the contents of the auth hash, add the following line of code at the beginning of your authentications/sessions/callbacks controller action:
render :text => "<pre>" + env["omniauth.auth"].to_yaml and return
You'll see that there is no phone field at auth.extra.raw_info or auth.info.

Devise + RSpec : How to authenticate a user?

I am using Devise for member authentication.
I need to test the log in scenario for a member. sign_in(member) doesn't check authentication information - it just signs in the member thats why i am using authenticate_member! method for authentication as suggested in Github Issue: How to authenticate the user with warden/devise in a customized way? .
BUT I get an exception saying ArgumentError: uncaught throw :warden.
#spec/features/member.rb
FactoryGirl.define do
factory :member do
email "john#gmail.com"
password "12345678"
firstname "John"
lastname "Doe"
location "United States"
end
end
#spec/controllers/sessions_spec.rb
it "authenticate member" do
create(:member)
#request.env["devise.mapping"] = Devise.mappings[:member]
#request.env["warden"] = warden
controller.allow_params_authentication!
expect(controller.authenticate_member!(:scope => :member, :force => true)).not_to be nil
end
I also tried to set controller.resource = FactoryGirl.attributes_for(:member) thinking that resource is not set but then i get exception as
NoMethodError: protected method 'resource=' called for #<Devise::SessionsController:0x007ffc4e5bab10>
How to resolve this issue?
I posted a github issue on Devise regarding this question: Github Issue: RSpec with Devise : How to authenticate a user?
I was advised to use a post to sessions#create in order to simulate the POST request from the sign in form.
Here is the alternative solution:
#spec/controllers/sessions_spec.rb
it "authenticate member" do
create(:member)
#request.env["devise.mapping"] = Devise.mappings[:member]
#request.env["warden"] = warden
## Added post request to "create"
post :create, :member => FactoryGirl.attributes_for(:member)
## Added new expectations
expect(controller.member_signed_in?).to be true
expect(controller.current_member.email).to eq "john#gmail.com"
## "session["warden.user.member.key"][0].first" stores the "id" of logged in member
## Replace "member" in "session["warden.user.member.key"][0].first" with your
## devise model name. For example: If your devise model name is customer then you
## need to check "session["warden.user.customer.key"][0].first"
expect(session["warden.user.member.key"][0].first).to eq member.id
end
NOTE:
I would have liked to get an explanation of why my previous code as suggested in question and as provided by Devise team in Github Issue: How to authenticate the user with warden/devise in a customized way? didn't work out which it should have. Feel free to post a new answer if anyone finds an explanation as to why my previous code for member authentication didn't work.

Facebook sends invalid userdata over Omniauth + Devise

Just two failed login attemps hit my mailbox. Looking at the data the problem was found quickly: Facebook is sending a 11 digit number instead of a email address to my Omniauth-Controller. So the User-Model can't be saved, because its created with a email constraint on the column in the migration file.
Anybody has a idea how I should proceed with the data in such cases? I don't really care if the user has a malformed email address, so I could theoretically just change the column to a normal string etc. Are there better solutions?
Bonus Questions:
Any other fields known to be unrelieable with the facebook + omniauth + devise
environment?
Any fields known to be unrelieable with the
google_oauth2 plugin?
I take it you are using a function similar to below...
def self.find_for_facebook_oauth(auth, signed_in_resource=nil)
user = User.where(:provider => auth.provider, :uid => auth.uid).first
unless user
user = User.create(name:auth.extra.raw_info.name,
provider:auth.provider,
uid:auth.uid,
email:auth.info.email,
password:Devise.friendly_token[0,20]
)
end
user
end
Are you sure auth.info.email is mapped to the correct parameter
EDIT:
Since the above is all fine. I guess the two options are remove the contraint as you say or perform your own check for string or that it contains an # or whatever and if the data passed is not the correct format you could insert a dummy email address.
This is how i solved it quick & dirty:
# facebook can send malformed/invalid email
email = auth[:info][:email]
unless email =~ /^(|(([A-Za-z0-9]+_+)|([A-Za-z0-9]+\-+)|([A-Za-z0-9]+\.+)|([A-Za-z0-9]+\++))*[A-Za-z0-9]+#((\w+\-+)|(\w+\.))*\w{1,63}\.[a-zA-Z]{2,6})$/i
email = "#{Time.now.to_i}#MALFORMED_EMAIL.com"

Rails Devise: Set password reset token and redirect user

In my app for a certain use case I create a new user (programmatically set the password) and send them a confirmation email.
I would like them to be able to change their password immediately after confirming (without having to enter the system generated one which I don't want to send them)
In effect I would like
1) System creates a new user account with generated password.
2) System sends confirmation email.
3) User clicks confirmation and is redirected to enter in their password (effectively send them to a URL like below)
Change my password
Any help / pointers would be great.
A simple way to have just one step for users to confirm email address and set initial password using the link you proposed...
Send one email your app generates, including a reset_password_token, and consider user's possession of that token confirmation of the validity of that email address.
In system account generation code, assuming User model is set up with :recoverable and :database_authenticatable Devise modules...
acct = User.new
acct.password = User.reset_password_token #won't actually be used...
acct.reset_password_token = User.reset_password_token
acct.email = "user#usercompany.com" #assuming users will identify themselves with this field
#set other acct fields you may need
acct.save
Make the devise reset password view a little clearer for users when setting initial password.
views/devise/passwords/edit.html.erb
...
<%= "true" == params[:initial] ? "Set your password" : "Reset your password" %>
...
Generated Email
Hi <%= #user.name %>
An account has been generated for you.
Please visit www.oursite.com/users/password/edit?initial=true&reset_password_token=<%= #user.reset_password_token %> to set your password.
No need to include :confirmable Devise module in your User model, since accounts created by your app won't get accessed without the reset_password_token in the email.
Devise will handle the submit and clear the reset_password_token field.
See devise_gem_folder/lib/devise/models/recoverable.rb and database_authenticatable.rb for details on reset_password_token method and friends.
If you want to use Devise :confirmable module rather than this approach, see the Devise wiki page.
In Rails 4.1, the following modification of Anatortoise House's reply works:
user = User.new
user.password = SecureRandom.hex #some random unguessable string
raw_token, hashed_token = Devise.token_generator.generate(User, :reset_password_token)
user.reset_password_token = hashed_token
user.reset_password_sent_at = Time.now.utc
user.email = 'user#usercompany.com'
user.save!
# Use a mailer you've written, such as:
AccountMailer.set_password_notice(user, raw_token).deliver
The email view has this link:
www.oursite.com/users/password/edit?initial=true&reset_password_token=<%= #raw_token %>
Here is my snippet for mailer preview
class Devise::MailerPreview < ActionMailer::Preview
def reset_password_instructions
user = User.last
token = user.send(:set_reset_password_token)
Devise::Mailer.reset_password_instructions(user, token)
end
end
You can call
user.send(:set_reset_password_token)
It may not be stable, as it's a protected method but it can work for your case. Just cover it with a test.
(tested on Devise v. 3.4)

Resources