How to get Koala to play nicely with Omniauth? - ruby-on-rails

I'm trying to get Koala to work with Omniauth. A User model logs in with Facebook using Omniauth and I want to use Koala as a client to pull the list of a user's friends that are using the app. I don't seem to be saving the tokens properly:
Controller
#friends = Array.new
if current_user.token
graph = Koala::Facebook::GraphAPI.new(current_user.token)
#profile_image = graph.get_picture("me")
#fbprofile = graph.get_object("me")
#friends = graph.get_connections("me", "friends")
end
DB Schema
create_table "users", :force => true do |t|
t.string "provider"
t.string "uid"
t.string "name"
t.datetime "created_at"
t.datetime "updated_at"
t.string "token"
end
User model has
def self.create_with_omniauth(auth)
create! do |user|
user.provider = auth["provider"]
user.uid = auth["uid"]
user.name = auth["user_info"]["name"]
end
end
Koala.rb initializer has:
module Facebook
CONFIG = YAML.load_file(Rails.root.join("config/facebook.yml"))[Rails.env]
APP_ID = CONFIG['app_id']
SECRET = CONFIG['secret_key']
end
Koala::Facebook::OAuth.class_eval do
def initialize_with_default_settings(*args)
case args.size
when 0, 1
raise "application id and/or secret are not specified in the config" unless Facebook::APP_ID && Facebook::SECRET
initialize_without_default_settings(Facebook::APP_ID.to_s, Facebook::SECRET.to_s, args.first)
when 2, 3
initialize_without_default_settings(*args)
end
end
alias_method_chain :initialize, :default_settings
end
Sessions controller has:
def create
auth = request.env["omniauth.auth"]
user = User.find_by_provider_and_uid(auth["provider"], auth["uid"]) || User.create_with_omniauth(auth)
session[:user_id] = user.id
session['fb_auth'] = request.env['omniauth.auth']
session['fb_access_token'] = omniauth['credentials']['token']
session['fb_error'] = nil
redirect_to root_url
end

The problem like you already know is that the fb_access_token is only available in the current session and not being available to Koala.
Does your user model have a column to store "token"? If not, then make sure you have that column in the user model. When you have that column in the user model, you will need to store something in it at the time you create the user (create_with_omniauth method in the User class). After successfull authorization from facebook you should find that the token field is populated with the facebook oauth token. If it is populated, then your Koala code should work. In this case there is no need to store the facebook credentials in the session.
If however you are not getting offline access from Facebook (which means the access is only provided for a short duration, then storing the facebook credentials in the session makes sense. In this case you should not use "current_user.token" but session["fb_auth_token"] instead with Koala.
Hope this helps!
So if you want offline access (long term storage of facebook authorization), change your model code to store fb_auth_token as below
# User model
def self.create_with_omniauth(auth)
create! do |user|
user.provider = auth["provider"]
user.uid = auth["uid"]
user.name = auth["user_info"]["name"]
user.token = auth['credentials']['token']
end
end
# SessionsController
def create
auth = request.env["omniauth.auth"]
user = User.find_by_provider_and_uid(auth["provider"], auth["uid"]) || User.create_with_omniauth(auth)
# Note i've also passed the omniauth object
session[:user_id] = user.id
session['fb_auth'] = auth
session['fb_access_token'] = auth['credentials']['token']
session['fb_error'] = nil
redirect_to root_url
end
If you have short term access then change your "other" controller to use sessions
# The other controller
def whateverthissactionis
#friends = Array.new
if session["fb_access_token"].present?
graph = Koala::Facebook::GraphAPI.new(session["fb_access_token"]) # Note that i'm using session here
#profile_image = graph.get_picture("me")
#fbprofile = graph.get_object("me")
#friends = graph.get_connections("me", "friends")
end
end

Avoid writing when you can just test this out:
https://github.com/holden/devise-omniauth-example/blob/master/config/initializers/devise.rb
I've used that app as a basis with success.
I've fixed a few issues but haven't committed on github however. But those are very minor. I think the example app works.
Your problem may not be Koala but the fact that the token was not saved so you can't query anything or even connect to Facebook.

Your issue looks to be that you're passing the wrong thing into Koala:
if #token = current_user.token
#graph = Koala::Facebook::GraphAPI.new(oauth_callback_url)
#friends = #graph.get_connections("me", "friends")
end
Try changing it to the following:
#friends = Array.new # I think it returns a straight array, might be wrong.
if current_user.token
graph = Koala::Facebook::GraphAPI.new(current_user.token)
#friends = graph.get_connections("me", "friends")
end

Related

devise token auth + Save Facebook info

I am using Devise auth token gem and ng-token auth for authenticating my single page app. Besides registering with email there is the option to sign-up via Facebook. It works.
My problem is that as for my Fb request scope I set to get the user_friends, email and public_info, however when I get the request I don't see the auth_hash request.env['omniauth.auth']. This is described in the omniauth-facebook gem documentation.
Basically I want to save what FB gives back about the user in my database. How would you do it?
I use this end-point with Ionic2 app, for Facebook Login requests on Rails 5 API, using Koala and Devise Token Auth so you should be able to replicate on any Rails 5 app:
def facebook
#graph = Koala::Facebook::API.new(oauth_params[:token], ENV["FACEBOOK_SECRET"])
#profile = #graph.get_object("me?fields=email,first_name,last_name,picture.type(large)")
unless #user = User.where(email: #profile["email"])&.first
#user = User.new(email: #profile["email"])
end
#
# Here you will make your logic for saving Facebook's data on your User model,
# customize accordingly
#
if #user.new_record?
p = SecureRandom.urlsafe_base64(nil, false)
#user.password = p
#user.password_confirmation = p
#
# "Passwordless" login, user must set password after registering,
# or be forever haunted by indecipherable SecureRandom value
#
#user.name = #profile["first_name"]+ " " + #profile["last_name"]
#user.remote_avatar_url = #profile["picture"]["data"]["url"]
#user.confirmed_at = Time.now
#user.uid = #profile["id"]
#user.provider = 'Facebook'
end
#client_id = SecureRandom.urlsafe_base64(nil, false)
#token = SecureRandom.urlsafe_base64(nil, false)
#user.tokens[#client_id] = {
token: BCrypt::Password.create(#token),
expiry: (Time.now + DeviseTokenAuth.token_lifespan).to_i
}
auth_header = #user.build_auth_header(#token, #client_id)
# update the response header
response.headers.merge!(auth_header)
#user.save!
if sign_in(:user, #user, store: false, bypass: false)
render json: {
data: #user.token_validation_response
}
end
end
Point a POST route to this and pass authResponse.accessToken from Facebook request as params[:token].
VoilĂ , authentication magic!
I don't have how to do this with Angular and Rails, but this is how I did it in my Rails app.
def show
#controller action
#FbInfoforFrontEnd = FbModel.from_omniauth(request.env["omniauth.auth"])
end
and in the FbModel model, I'd implement the mechanism to save the token and rest of the info like this
def self.from_omniauth(auth)
#FbModel model method
oauth = Koala::Facebook::OAuth.new(AppId, AppSecret)
new_access_info = oauth.exchange_access_token_info auth.credentials.token
new_access_token = new_access_info["access_token"]
new_access_expires_at = DateTime.now + new_access_info["expires"].to_i.seconds
return FbModel.where(provider: auth.provider,
uid: auth.uid)
.first_or_create(provider: auth.provider,
uid: auth.uid,
name: auth.info.name)
.update_attributes(oauth_expires_at: new_access_expires_at,
oauth_token: new_access_token)
end
I hope this answers your query

Rails: Cookie overflow with omniauth twitter sign up

I am using omniauth to let people sign up/sign in with Facebook and its working well ! But I wanted to add the omniauth-twitter gem to let them connect with Twitter.
I followed the same steps than when I set up the Facebook connect: https://github.com/plataformatec/devise/wiki/OmniAuth:-Overview
But when I signing up/in I get the following error:
ActionDispatch::Cookies::CookieOverflow in OmniauthCallbacksController#twitter
at the following URL:
http://localhost:3000/users/auth/twitter/callback?oauth_token=HRjON8J4bj9EcbjiELHcpHmSXo0cPd0wCHyuWG8ATZU&oauth_verifier=ZiZb1FAKZmNML1gVu5RKBLEGzbeAPPzC80QCpPDGU
I tried different things suggested on similar posts but none of these worked :(
Here is my configuration:
omniauth_callbacks_controller.rb => app/controllers/omniauth_callbacks_controller.rb
def twitter
# You need to implement the method below in your model (e.g. app/models/user.rb)
#user = User.find_for_twitter_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 => "twitter") if is_navigational_format?
else
session["devise.twitter_data"] = request.env["omniauth.auth"]
redirect_to new_user_registration_url
end
end
user.rb => app/models/user.rb
def self.find_for_twitter_oauth(auth)
where(auth.slice(:provider, :uid)).first_or_create do |user|
user.provider = auth.provider
user.uid = auth.uid
user.email = auth.info.email
user.password = Devise.friendly_token[0,20]
user.name = auth.info.name # assuming the user model has a name
end
end
def self.new_with_session(params, session)
super.tap do |user|
if data = session["devise.twitter_data"] && session["devise.twitter_data"]["extra"]["raw_info"]
user.email = data["email"] if user.email.blank?
end
end
end
devise.rb => app/config/initializers/devise.rb
Rails.application.config.middleware.use OmniAuth::Builder do
provider :twitter, "KEY, "KEYPASSWORD
end
Any ideas what's wrong?
As Michael says in the comments, you're storing a large hash in the session and it's too big (you're using the default CookieStore and cookies can only contain 4KB of data). That hash provided by omniauth has all the data returned by twitter, which can be quite a lot. For example, see the README: https://github.com/arunagw/omniauth-twitter#authentication-hash
If the code in your question is all the code relating to twitter login, then it looks like you only need to keep the email in the session as that is all that is used by your new_with_session code. So your line in the else in twitter which is:
session["devise.twitter_data"] = request.env["omniauth.auth"]
could be something like:
session["devise.twitter_data"] = request.env["omniauth.auth"].select { |k, v| k == "email" }
However the major flaw with this is that twitter doesn't return an email address for a user, so data["email"] will always be nil in new_with_session anyway! So it's pointless keeping anything in the session if you are only later interested in the email which is never returned by twitter. Perhaps you instead want to retrieve a name to help prefill the registration form instead of the email address. In this case, you could just keep that in the hash from omniauth. If you want to keep a few things in the hash, then instead of selecting them all to put in the session, you could do something like:
session["devise.twitter_data"] = request.env["omniauth.auth"].delete_if("extra")
which will remove the "extra" nested hash which could help everything else to fit in the session.
For a complete solution you'll have to consider messy situations like dealing with people who have signed in with Facebook and then come and sign in with Twitter and want to use the same email address and merge with their existing account on your system.
In any case, note that if you are using Rails 3 then the session cookie is not encrypted so the user or anyone with access to their computer could read the contents of the cookie with whatever data from twitter you end up keeping in there. If you're using Rails 4, then the cookie should be encrypted to protect against that.

Ruby on Rails no such column: authentication.provider: Omniauth

I was following this tutorial on Omniauth: http://railscasts.com/episodes/235-omniauth-part-1?view=asciicast
I keep getting this error:
no such column: authentication.provider:
Now the main thing I want to know is why "provider" isn't being accepted. It exists in the class... the authentications database exists... so why is it saying it isn't there?
Here's my authentications controller:
class AuthenticationsController < InheritedResources::Base
def index
#authentications = current_user.authentications if current_user
end
def create
#user = User.where(authentication: auth).first_or_create(:provider => auth['provider'], :uid => auth['uid'])
self.current_user = #user
# auth = request.env["omniauth.auth"] current_user.authentications.create(:provider => auth['provider'], :uid => auth['uid'])
flash[:notice] = "Authentication successful."
redirect_to authentications_url
end
def auth
request.env['omniauth.auth']
end
def destroy
#authentication = current_user.authentications.find(params[:id])
#authentication.destroy
flash[:notice] = "Successfully destroyed authentication."
redirect_to authentications_url
end
end
I can assure you I have a model called authentication and that this model has a provider and uid field. I've also tried where(authentications: auth) and where(auth: auth)
each with no luck.
Any ideas would be appreciated.
UPDATE
authentication.rb (model)
class Authentication < ActiveRecord::Base
attr_accessible :create, :destroy, :index, :provider, :uid, :user_id
belongs_to :user
end
UPDATE 2
I'm basically attempting to adapt this tutorial to rails 3.2.
The original line from the tutorial is commented out above.
UPDATE 3
Here is the entire first line of error:
SQLite3::SQLException: no such column: authentication.provider: SELECT "users".* FROM "users" WHERE "authentication"."provider" = 'facebook' AND "authentication"."uid" = '2222222' AND "authentication"."info" = '--- !ruby/hash:OmniAuth::AuthHash::InfoHash
Hate to be a burden... but the clock's really ticking, my ass is on the line, and I'm about to go completely insane trying to figure this out. If you can tell me just why provider isn't being accepted I'm sure I can figure out the rest.
your create action has not sense
User.where(authentication: auth) converts to SELECT * FROM users WHERE authentication = a_hash
You shoul do something like
auth1 = Authentication.where(provider: auth['provider'], uid: auth['uid']).first
if !auth1.nil?
current_user = auth.user
else
user = User.new
user.authentications.build(provider: auth['provider'], uid: auth['uid'])
user.save!
current_user = user
end
Since you are just adding a record in the authentications table, I am unable to understand why you are reassigning this.current_user. Also is current_user a helper method or a member, if it's a member where is it declared?
Don't you just want to create an authentication for the current user as such?:
def create
current_user.authentications.first_or_create(:provider => auth['provider'], :uid => auth['uid'])
flash[:notice] = "Authentication successful."
redirect_to authentications_url
end
This finds the first authentication record by provider and uid, if not found then creates that authentication record.
Also by that error, I hope you have figured out the answer to this question:
Now the main thing I want to know is why "provider" isn't being
accepted. It exists in the class... the authentications database
exists... so why is it saying it isn't there?
It is because you are calling first_or_create() on User object, not Authentication.
I also faced this issue recently. At first I thought I had forgotten to add a provider column to users table, but that wasn't it.
This is how I eventually solved it:
def self.from_omniauth(auth)
where(provider: auth["provider"], uid: auth["uid"]).first_or_create do |user|
user.email = auth["info"]["email"]
user.password = Devise.friendly_token[0, 20]
user.logo = auth["info"]["image"]
# if you use confirmable, since facebook validates emails
# skip confirmation emails
user.skip_confirmation!
end
end
auth is a hash like the one below, so instead of auth.provider, I used auth["provider"] etc:
omniauth.auth: {"provider"=>"facebook", "uid"=>"11111111111111", "info"=>{"email"=>"some#email.com", "image"=>"http://graph.facebook.com/v2.6/11111111111111/picture"}, "credentials"=>{"token"=>"sometoken", "expires_at"=>1506680013, "expires"=>true}, "extra"=>{"raw_info"=>{"email"=>"some#email.com", "id"=>"11111111111111"}}}

Creating a user profile with Omniauth hash

I'm trying to pull out profile information from my user model. Currently I'm using Omniauth (twitter & facebook) to create users and just writing all the information to the user table... this works great, however I was told that proper Rails convention is to separate the user & profile.
After a couple of days of trial and error, I'm having trouble getting the has_one relationship working while creating a profile with the users info from the Omniauth hash.
I've tried to user a before_filter to automagically create a profile before the user is create, which properly creates the user_id foreign key in the profiles table, however I can't seem to get any of the omniauth hash data to write.
my sessions controller:
def create
auth = request.env["omniauth.auth"]
if user = User.find_by_provider_and_uid(auth["provider"], auth["uid"])
redirect_to root_url, :notice => 'Signed In'
else
user = User.create_with_omniauth(auth)
redirect_to profile_path(user), :notice => 'Please verify your profile'
end
in my user model:
before_create :create_profile
def self.create_profile(auth)
create! do |profile|
profile.user_id = session[:user_id]
profile.name = auth["info"]["name"]
profile.email = auth["info"]["email"]
profile.nickname = auth["info"]["nickname"]
profile.location = auth["info"]["location"]
profile.image = auth["info"]["image"]
end
end
...
The other option I tried was to write the profile data with the profile model... which works for the Omniauth hash data, but wont write the correct user_id (so no foreign key)
profile.rb
def self.create_profile_with_omniauth(auth)
create! do |profile|
profile.user_id = session[:user_id]
profile.name = auth["info"]["name"]
profile.email = auth["info"]["email"]
profile.nickname = auth["info"]["nickname"]
profile.location = auth["info"]["location"]
profile.image = auth["info"]["image"]
end
end

How do I prevent calls to the Facebook API for non-first sessions?

My app authenticates users through Facebook with OmniAuth and it works fine but every time a user has to create a session and sign in, the request to Facebook takes up to a minute because it is calling a lot of information through the API. How do I refactor the code below to only call the graph objects (profile, likes, friends) for when it does || User.create_with_omniauth(auth,omniauth)?
def create
auth = request.env["omniauth.auth"]
omniauth = request.env["omniauth.auth"]
user = User.find_by_provider_and_uid(auth["provider"], auth["uid"]) || User.create_with_omniauth(auth,omniauth)
session[:user_id] = user.id
session['fb_auth'] = request.env['omniauth.auth']
session['fb_access_token'] = omniauth['credentials']['token']
session['fb_error'] = nil
#graph = Koala::Facebook::GraphAPI.new(current_user.token)
current_user.profile = #graph.get_object("me")
current_user.likes = #graph.get_connections("me", "likes")
current_user.friends = #graph.get_connections("me", "friends")
current_user.save
redirect_to root_url
end
The easiest way to do this is by extracting out the last part of what you are doing into an asynchronous job. There doesn't seem to be any need for you to have all this information prior to creating the user, and any requests to external services that are not necessary for the immediate needs should be offloaded.
I am doing the same thing with Facebook currently and I am using Redis/Resque to update the users details after they are created/authenticated.

Resources