Devise+OmniAuth for Facebook Sign Up on Remote Server - ruby-on-rails

I am using Devise + Omniauth to enable Facebook signup in my application. When I was developing it, I encountered no problems. Same with deploying it to my remote server. The problem is, other people keep encountering the same error:
TypeError (no implicit conversion of Symbol into Integer):
app/models/user.rb:67:in `find_for_facebook_oauth'
app/controllers/users/omniauth_callbacks_controller.rb:4:in `facebook'
I have the following code for the User model user.rb:
def self.find_for_facebook_oauth( data, signed_in_resource=nil)
user = User.where(:email => data.info.email).first
unless user
params =
{
:user =>
{
:username => data.uid,
:email => data.info.email,
:password => Devise.friendly_token[0,20],
:user_profile_attributes =>
{
:first_name => data.extra.raw_info.first_name,
:last_name => data.extra.raw_info.last_name,
:remote_image_url => data.extra.raw_info.image,
},
:user_auths_attributes =>
{
:uid => data.uid,
:provider => data.provider
}
}
}
user = User.create!(params[:user])
end
return user
end
Where line 67 is the user = User.create!(params[:user])
And omniauth_callbacks_controller.rb:
def facebook
# You need to implement the method below in your model (e.g. app/models/user.rb)
#user = User.find_for_facebook_oauth(request.env["omniauth.auth"])
if #user.persisted?
sign_in_and_redirect #user, :event => :authentication #this will throw if #user is not activated
set_flash_message(:notice, :success, :kind => "Facebook") if is_navigational_format?
else
session["devise.facebook_data"] = request.env["omniauth.auth"]
redirect_to new_user_registration_url
end
end
Where line 4 is #user = User.find_for_facebook_oauth(request.env["omniauth.auth"])
The server logs also show the GET parameters:
Parameters: {"code"=>"[some long string of number and letters]", "state"=>"[another string of numbers and letters]"}
Update:
The logger outputs the following for request.env["omniauth.auth"]:
#<OmniAuth::AuthHash credentials=#<OmniAuth::AuthHash expires=true expires_at=1401992074 token="*"> extra=#<OmniAuth::AuthHash raw_info=#<OmniAuth::AuthHash email="*" first_name="*" gender="male" id="*" last_name="*" link="https://www.facebook.com/*" locale="en_US" name="*" timezone=8 updated_time="2014-04-05T09:29:22+0000" username="*" verified=true>> info=#<OmniAuth::AuthHash::InfoHash email="*" first_name="*" image="http://graph.facebook.com/*/picture?type=square" last_name="*" name="*" nickname="*" urls=#<OmniAuth::AuthHash Facebook="https://www.facebook.com/*"> verified=true> provider="facebook" uid="*">
Update 2:
Logging the params[:user] provides the following values:
Params: {:username=>"*", :email=>"*", :password=>"iePVLt7XEWk4YwPjja6n", :user_profile_attributes=>{:first_name=>"*", :last_name=>"*", :remote_image_url=>"http://graph.facebook.com/*/picture?type=square"}, :user_auths_attributes=>{:uid=>"*", :provider=>"facebook"}}

Update your params hash as below:
params =
{
:user =>
{
:username => data.uid,
:email => data.info.email,
:password => Devise.friendly_token[0,20],
:user_profile_attributes =>
{
:first_name => data.extra.raw_info.first_name,
:last_name => data.extra.raw_info.last_name,
:remote_image_url => data.info.image ## Removed comma and updated the method
},
:user_auths_attributes =>
[{
:uid => data.uid,
:provider => data.provider
}] ## Enclosed within array [] brackets
}
}
Looking at the params hash given by you, I can tell that a User and Profile have a 1-1 Relationship whereas User and Auths has a 1-M Relationship. In that case, user_auths_attributes must be passed as an Array.
TypeError (no implicit conversion of Symbol into Integer)
You were getting the above error because user_auths_attributes was being interpreted as an array and not a hash. So when Ruby saw params[:user][:user_auths_attributes][:uid] it was trying to take the last key and turn it into params[:user][:user_auths_attributes][0] or at least find some integer value it could be converted to index the Array.

I found only this issue:
:remote_image_url => data.extra.raw_info.image # In data I see only data.info.image
replace with
:remote_image_url => data.info.image
But it is not a solution for your question.
Try to debug data from params[:user]. From exception it looks like that you use some Hash on property which is Integer.

Related

Getting 'undefined method `merge!' when merging SoundCloud exchange token

I'm working on an app that interacts with SoundCloud and I'm having an issue when I try to save the exchange_token that I'm getting back from the server (among other things) and I really could use some assistance.
According to the error I'm getting:
undefined method `merge!' for nil:NilClass
The problem apparently lies with line 10 in my sclouds_controller.rb file (included below):
soundcloud_client.exchange_token(:code => params[:code])
Which is calling a method in the SoundCloud gem that I'm using. Here's the line in the SoundCloud gem that the error originates from:
params.merge!(client_params)
That can be found on line 23 of the following method (taken from the client.rb file in the SoundCloud gem):
def exchange_token(options={})
store_options(options)
raise ArgumentError, 'client_id and client_secret is required to retrieve an access_token' if client_id.nil? || client_secret.nil?
client_params = {:client_id => client_id, :client_secret => client_secret}
params = if options_for_refresh_flow_present?
{
:grant_type => 'refresh_token',
:refresh_token => refresh_token,
}
elsif options_for_credentials_flow_present?
{
:grant_type => 'password',
:username => #options[:username],
:password => #options[:password],
}
elsif options_for_code_flow_present?
{
:grant_type => 'authorization_code',
:redirect_uri => #options[:redirect_uri],
:code => #options[:code],
}
end
params.merge!(client_params)
response = handle_response(false) {
self.class.post("https://#{api_host}#{TOKEN_PATH}", :query => params)
}
#options.merge!(:access_token => response.access_token, :refresh_token => response.refresh_token)
#options[:expires_at] = Time.now + response.expires_in if response.expires_in
#options[:on_exchange_token].call(*[(self if #options[:on_exchange_token].arity == 1)].compact)
response
end
However, if I throw a 'raise' in my sclouds_controller.rb file like this:
def connected
if params[:error].nil?
raise
soundcloud_client.exchange_token(:code => params[:code])
Then, in the console, manually paste in the following line:
soundcloud_client.exchange_token(:code => params[:code])
I get back the following response (which, to me, appears to be successful):
$ #<SoundCloud::HashResponseWrapper access_token="xxxxx" expires_in=21599 refresh_token="xxxxx" scope="*">
Any idea why this is happening? I'm trying to learn what I'm doing wrong, especially since I'm not sure if I'm going about this in the right way. Here's some of my code for a little more context. Thanks in advance!
sclouds_controller.rb:
class ScloudsController < ApplicationController
before_action :authenticate_user!, only: [:connect, :connected]
def connect
redirect_to soundcloud_client.authorize_url(:display => "popup")
end
def connected
if params[:error].nil?
soundcloud_client.exchange_token(:code => params[:code])
unless user_signed_in?
flash[:alert] = 's'
redirect_to :login
end
current_user.update_attributes!({
:soundcloud_access_token => soundcloud_client.access_token,
:soundcloud_refresh_token => soundcloud_client.refresh_token,
:soundcloud_expires_at => soundcloud_client.expires_at
})
end
redirect_to soundcloud_client.redirect_uri
end
def disconnect
login_as nil
redirect_to root_path
end
private
def soundcloud_client
return #soundcloud_client if #soundcloud_client
#soundcloud_client = User.soundcloud_client(:redirect_uri => 'http://localhost:3000/sclouds/connected/')
end
end
user.rb:
class User < ActiveRecord::Base
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :trackable, :validatable
attr_accessor :soundcloud_access_token, :soundcloud_refresh_token, :soundcloud_expires_at
has_one :scloud
#SOUNDCLOUD_CLIENT_ID = 'xxxxx'
#SOUNDCLOUD_CLIENT_SECRET = 'xxxxx'
#REDIRECT_URI = 'xxxxx'
def self.soundcloud_client(options={})
options = {
:client_id => #SOUNDCLOUD_CLIENT_ID,
:client_secret => #SOUNDCLOUD_CLIENT_SECRET,
:redirect_uri => #REDIRECT_URI
}.merge(options)
Soundcloud.new(options)
end
def soundcloud_client(options={})
client = self.class.soundcloud_client(options)
options= {
:access_token => soundcloud_access_token,
:refresh_token => soundcloud_refresh_token,
:expires_at => soundcloud_expires_at
}.merge(options)
client.on_exchange_token do
self.update_attributes!({
:soundcloud_access_token => client.access_token,
:soundcloud_refresh_token => client.refresh_token,
:soundcloud_expires_at => client.expires_at
})
end
client
end
end
options= {
:access_token => soundcloud_access_token,
:refresh_token => soundcloud_refresh_token,
:expires_at => soundcloud_expires_at
}.merge(options)
How does this work?
You're trying to set the options local var (which is a duplicate of the options argument in your method), and then you're trying to merge options? Looks like a self-referential loop to me...
Method
Secondly, you mention the error is caused by this line:
exchange_token(:code => params[:code])
I can't see this method in your documentation? The merge! method is obviously being caused inside this method somewhere - we need to know where it's being called, so we can fix it!
I can see one mention of merge, which I've queried above
Update
Thanks for posting the code!
I think #mischa is right - but I'll keep this code to show you what I'd look at..
I can only give an opinion, as I've never used this gem before -There are two calls to merge! in the gem code:
params.merge!(client_params)
#options.merge!(:access_token => response.access_token, :refresh_token => response.refresh_token)
I'd look at this:
Params
The params hash is populated locally like this:
params = if options_for_refresh_flow_present?
{
:grant_type => 'refresh_token',
:refresh_token => refresh_token,
}
elsif options_for_credentials_flow_present?
{
:grant_type => 'password',
:username => #options[:username],
:password => #options[:password],
}
elsif options_for_code_flow_present?
{
:grant_type => 'authorization_code',
:redirect_uri => #options[:redirect_uri],
:code => #options[:code],
}
end
This could be the cause of the problem, as it is populated with elsif (not else). This means you have to make sure you're passing the right codes to the system
#options
#options is referenced but not declared
I imagine it's declared in another part of the gem code, meaning you need to make sure it's there. This will likely be set when the gem initializes (I.E when you set up the authentication between SC & your app)
If you've not got the #options set up correctly, it will probably mean you're not calling the gem correctly, hence the error.
Try mischa's fix, and see if that cures the issue for you.
Initializer
Something to note - convention for including gems in your system is to use an initializer
If you set up an initializer which creates a new constant called SOUNDCLOUD, you can then reference that throughout your app (curing any errors you have here)
I can write some code for you on this if you want
#RickPeck got very close I think, as the problem really lies in this code excerpt:
params = if options_for_refresh_flow_present?
{
:grant_type => 'refresh_token',
:refresh_token => refresh_token,
}
elsif options_for_credentials_flow_present?
{
:grant_type => 'password',
:username => #options[:username],
:password => #options[:password],
}
elsif options_for_code_flow_present?
{
:grant_type => 'authorization_code',
:redirect_uri => #options[:redirect_uri],
:code => #options[:code],
}
end
#options is populated in the line store_options(options). So the problem is that neither options_for_refresh_flow_present?, options_for_credentials_flow_present?, nor options_for_code_flow_present? return true.
The relevant option for your code is code flow:
def options_for_code_flow_present?
!!(#options[:code] && #options[:redirect_uri])
end
which expects options to have both :code and :redirect_uri. In your code you pass only :code. Add a :redirect_uri and you should be good to go.
What #Mischa suggests will probably fix that for you, as your :redirect_uri was nil when you set it...

validates_acceptance_of terms checkbox and Facebook connect

How can you force terms and condition acceptance whith facebook connect ?
In my OmniauthCallbacksController i tried
#user_ipad.terms = true
Validation of the User still fails and the user gets redirected to the sign_up form with an error message.
/app/controllers/OmniauthCallbacksController
#user_ipad = UserIpad.new(:password => Devise.friendly_token[0,20])
#user_ipad.update_with_facebook_infos(omniauth)
#user_ipad.authentication_ways.build(:provider => omniauth['provider'], :uid => omniauth['uid'])
#user_ipad.terms = true
if #user_ipad.save()
sign_in_and_redirect #user_ipad, :event => :authentication
else
session["devise.facebook_data"] = omniauth
render :action=>'new', :controller=>'user_ipads', :layout => 'empty_layout'
end
/app/models/User
attr_accessible :terms
...
validates_acceptance_of :terms, :allow_nil => false, :message => "Vous devez accepter les conditions générales d'utilisation", :on => :create
Thanks for your help,
Vincent
validates_acceptance_of has an "accept" option. Rails doc says :
:accept - Specifies value that is considered accepted. The default
value is a string “1”, which makes it easy to relate to an HTML
checkbox. This should be set to true if you are validating a database
column, since the attribute is typecast from “1” to true before
validation.
So in your case, you should replace
#user_ipad.terms = true
with :
#user_ipad.terms = "1"

Rails: retrieving image from Facebook after Omniauth login with Devise

I setup Facebook login with Devise and omniauth with these instructions https://github.com/plataformatec/devise/wiki/OmniAuth:-Overview
The Devise wiki gives some instructions for getting facebook info from the hash stored in this variable request.env['omniauth.auth'] See bottom for the hash.
For example, Devise wiki has these two methods for the User.rb model
def self.find_for_facebook_oauth(access_token, signed_in_resource=nil)
data = access_token.extra.raw_info
if user = User.where(:email => data.email).first
user
else # Create a user with a stub password.
User.create!(:email => data.email, :password => Devise.friendly_token[0,20])
end
end
def self.new_with_session(params, session)
super.tap do |user|
if data = session["devise.facebook_data"] && session["devise.facebook_data"]["extra"]["raw_info"]
user.email = data["email"]
end
end
end
So, using the hash below, I added the following to those two methods to get the name and image
def self.find_for_facebook_oauth(access_token, signed_in_resource=nil)
data = access_token.extra.raw_info
if user = User.where(:email => data.email).first
user
else # Create a user with a stub password.
User.create!(:email => data.email, :password => Devise.friendly_token[0,20], :name => data.name, :image => access_token.info.image) #I added access_token.info.image based on first answer
end
end
def self.new_with_session(params, session)
super.tap do |user|
if data = session["devise.facebook_data"] && session["devise.facebook_data"]["extra"]["raw_info"]
user.email = data["email"]
user.name = data["name"]
user.image = access_token.info.image #i changed this based on first answer below
end
end
end
Then in my view, I added the following to show the user name and image
<p>Name:<%= user.name %></p>
<p>Image: <%= image_tag user.image %>
However, only the name is showing. No image.
In my database, I have a name and an image column. The name from Facebook is being stored, but the image column says 'nil'
Any ideas how I can get the image to work?
Hash stored in request.env['omniauth.auth'] https://github.com/mkdynamic/omniauth-facebook/blob/master/lib/omniauth/strategies/facebook.rb#L31-47
info do
prune!({
'nickname' => raw_info['username'],
'email' => raw_info['email'],
'name' => raw_info['name'],
'first_name' => raw_info['first_name'],
'last_name' => raw_info['last_name'],
'image' => "#{options[:secure_image_url] ? 'https' : 'http'}://graph.facebook.com/#{uid}/picture?type=square",
'description' => raw_info['bio'],
'urls' => {
'Facebook' => raw_info['link'],
'Website' => raw_info['website']
},
'location' => (raw_info['location'] || {})['name'],
'verified' => raw_info['verified']
})
end
The image can be found at env["omniauth.auth"]["info"]["image"]. So in your case, access_token.info.image.
If you want to take a good look at the hash of nested hashes returned and see for yourself where everything is, put this as the first line of your callback controller:
render :text => "<pre>" + env["omniauth.auth"].to_yaml and return
EDIT: Ok, so here's what you need to do:
def self.find_for_facebook_oauth(omniauth)
if user = User.find_by_email(omniauth.info.email)
if omniauth.info.image.present?
user.update_attribute(:image, omniauth.info.image)
end
user
else # Create a user with a stub password.
User.create!(:email => omniauth.info.email,
:name => omniauth.info.name,
:image => omniauth.info.image,
:password => Devise.friendly_token[0,20])
end
end
As for the other method, if I'm not mistaken, it should look like this:
def self.new_with_session(params, session)
super.tap do |user|
if omniauth = session["devise.facebook_data"]
user.email = omniauth.info.email
user.name = omniauth.info.name
user.image = omniauth.info.image
end
end
end
But when is this method used? It's used by Devise when something goes wrong when creating your user. Imagine that the authentication provider doesn't give you an email (Twitter, for example, does this), what can you do? Well, you can redirect the user to your sign up page where he can complete the signup process. But if you redirect the user, you lose the data received by the oauth. The solution is to put this data into the session.
So in your controller, you should have something like:
if user.save
sign_in_and_redirect user, :event => :authentication
else
session["devise.facebook_data"] = env["omniauth.auth"]
redirect_to new_user_registration_url
end
Another problem, however, is that most of the times the data returned by the authentication provider is too big to fit in the session, so we have to pick exactly what we want to put in the session. Since you are only getting a name and an image, you can trim the extra info like so:
session["devise.facebook_data"] = env["omniauth.auth"].except('extra')

Cannot Convert Symbol Into Integer

I'm trying to implement a simple login system in Rails, but when I try to display the username of a logged in user, I get this error:
can't convert Symbol into Integer
Extracted source (around line #60):
57: </ul>
58: <% if session[:logged_in] %>
59: <% user = session[:user] %>
60: <p class="pull-right">Howdy, <strong><%= user[:username] %></strong>!</p>
61: <% end %>
62: </div>
63: </div>
My model code is here:
require 'digest'
class User < ActiveRecord::Base
before_save {|user| user.password = Digest::SHA1.hexdigest(user.password)}
attr_accessible :username, :password, :email
validates_length_of :username, :password, :minimum => 7
validates_presence_of :username,:password,:email, :on => :create
validates_format_of :email, :with => /^([^#\s]+)#((?:[-a-z0-9]+\.)+[a-z]{2,})$/i
end
This is how I set session[:user]:
def create
if User.find(:all, :conditions => {:username => params[:username], :password => Digest::SHA1.hexdigest(params[:username])})
user = User.find(:all, :conditions => {:username => params[:username], :password => Digest::SHA1.hexdigest(params[:password])})
session[:user] = user
session[:logged_in] = true
redirect_to(:root, :notice => "Thanks for logging in!")
else
redirect_to(:new, :notice => "You supplied an invalid username/password combination.")
end
end
Probably session[:user] is not a Hash, as you expect it to be, but an Array. Thus subscripting it with anything other than an integer is not valid.
How to fix this? Change the code that is actually setting the session variable (like session[:user] = XYZ).
EDIT: User.find(:all, ...) returns an array, so as I assumed, you are assigning an array to session[:user]. You should only assign the first user found (and in fact, there should be only one matching the criteria). Even better, you should only store the username in the session and fetch it from the database if needed:
def create
user = User.where(:username => params[:username], :password => Digest::SHA1.hexdigest(params[:username])).first
if user
session[:user_id] = user.id
else
redirect_to(:new, :notice => "You supplied an invalid username/password combination.")
end
end
Then in the action associated with your view:
def ...
#user = User.find(session[:user_id])
unless #user
# redirect to error page, user was deleted in the meantime
end
end
Then in the view:
<%= #user.username %>
Dumping the whole User object into your session is a bad idea, and is probably why you're not getting back what you expect. You should implement something like #to_session on your User class that returns a hash with the minimum required information. Something like:
def to_session
{:id => id, :username => username, :email => email}
end
Then when you set the session:
session[:user] = user.to_session

Authlogic Create new session without password

I'm trying to build Facebook OAuth into my existing Authlogic login system. I have the OAuth part complete, and stored the facebook access_token. The problem I'm facing is to actually log the user in (create a session) without the user typing in their password.
#facebook's OAuth callback
def callback
access_token = client.web_server.get_access_token(params[:code], :redirect_uri => redirect_uri)
fb_user = JSON.parse(access_token.get('/me'))
#user = User.find_by_facebook_id(fb_user["id"]) || User.find_by_email(fb_user["email"]) || User.new
#user.update_attributes({
:facebook_id => fb_user["id"],
:first_name => fb_user["first_name"],
:last_name => fb_user["last_name"],
:gender => fb_user["gender"],
:email => fb_user["email"],
:timezone => fb_user["timezone"],
:locale => fb_user["locale"],
:facebook_url => fb_user["link"],
:facebook_access_token => access_token.token
}) #unless #user.updated_at < 2.days.ago
# TODO: set current_user
# Maybe something like this?
# #user_session = UserSession.new({
# :remember_me => true,
# :password =>"[FILTERED]",
# :email => email
# }).save
flash[:success] = "Welcome, #{#user.name}"
redirect_to :root
end
Nevermind I figured it out. It was in the README the whole time.
UserSession.new(#user, true) //true = persistent session

Resources