devise token auth + Save Facebook info - ruby-on-rails

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

Related

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.

How to generate reset password token

I am using devise gem for authentication.
In my application admin will create the users, so I want the user's reset password link when admin creates users.
This is my action:-
def create
#user = User.new(user_params)
#user.password = '123123123'
#user.password_confirmation = '123123123'
if #user.save
#user.update_attributes(:confirmation_token => nil,:confirmed_at => Time.now,:reset_password_token => (0...16).map{(65+rand(26)).chr}.join,:reset_password_sent_at => Time.now)
UserMailer.user_link(#user).deliver
redirect_to users_path
else
render :action => "new"
end
end
This is my link to reset a user's password
But I am getting reset password token is invalid when I open the link and update the password.
If you are using devise why are you creating your own password reset token?
Devise has a feature for that.
http://rubydoc.info/github/plataformatec/devise/master/Devise/Models/Recoverable
In case you wonder this is what devise does when the user wants to reset his password:
raw, enc = Devise.token_generator.generate(self.class, :reset_password_token)
self.reset_password_token = enc
self.reset_password_sent_at = Time.now.utc
self.save(validate: false)
self is a User object here
In your URL you then have to pass raw as reset_password_token parameter
You can generate a token with:
Devise.token_generator.generate(User, :reset_password_token)
Though this is just a useless string by itself. You need to attach it to the user if you actually want to use it in a link to reset passwords:
user.reset_password_token = hashed_token
user.reset_password_sent_at = Time.now.utc
Then send them an email with the link:
edit_password_url(#user, reset_password_token: #token)
You can use user.send_reset_password_instructions for that.
If you don't want it to send the instructions, just set and store the token you can call the private method in devise recoverable concern set_reset_password_token.
You can do this by doing something like user.send(:set_reset_password_token).
To get the url to reset the password using Devise I use this snippet of code:
user = User.find(id)
raw, enc = Devise.token_generator.generate(User, :reset_password_token)
user.update_columns(reset_password_token: enc, reset_password_sent_at: Time.current)
puts Rails.application.routes.url_helpers.edit_user_password_url(reset_password_token: raw, host: 'localhost:3000')
Expanding upon #Rails Fan's answer. The specific method that handles the password reset in Recoverable module is a protected method set_reset_password_token .
You can access it by the following code and it will return the token directly.
## your model.send(:set_reset_password_token)
user.send(:set_reset_password_token)

registration of new users with Devise and omniauth-google-oauth2

I've trying to manage user sign up with google account for my rails 4.0.0 app. Devise works perfectly. And there is working sign in with Google Account for existing users. But I have some difficulties with new user registration using Google Oauth 2. For example: i've got google account "example#google.com". It's logged in on my current PC. And when I try to sign up with this account to my app it generates blank register form. If I dont manually provide email, login, full name, etc. - I've got error message that they "cannot be blank". I guess solution is create default value to text fields to fetch user details.
So, my question is how can I provide values for variables in view that equals variables from google account?
Email field in form_for in new user registration:
= f.email_field :email, :autofocus => true, :value => 'how can i put auth.info.email here?'
omniauth_callbacks_controller.rb:
class OmniauthCallbacksController < Devise::OmniauthCallbacksController
def google_oauth2
user = User.from_omniauth(request.env["omniauth.auth"])
if user.persisted?
flash.notice = "Signed in Through Google!"
sign_in_and_redirect user
else
session["devise.user_attributes"] = user.attributes
flash.notice = "You are almost Done! Please provide a password to finish setting up your account"
redirect_to new_user_registration_url
end
end
end
omniauth method from user model:
def self.from_omniauth(auth)
if user = User.find_by_email(auth.info.email)
user.provider = auth.provider
user.uid = auth.uid
user
else
where(auth.slice(:provider, :uid)).first_or_create do |user|
user.provider = auth.provider
user.uid = auth.uid
user.full_name = auth.info.name
user.email = auth.info.email # THIS (user.email) value i want to provide to my registration form as default value
user.birthday = auth.info.birthday
user.avatar = auth.info.image
end
end
end
I had the same problem with GitHub you can take a look at my user model
https://github.com/flower-pot/pastebin/blob/master/app/models/user.rb

Rails + OmniAuth Facebook: how to obtain Access Token?

I am trying to fetch the list of friends from Facebook. Sign in through Facebook is not a problem, but the problem is to fetch person's friends - because of access token.
puts request.env["omniauth.auth"].inspect
puts '==='
#user = User.find_for_facebook_oauth(request.env["omniauth.auth"], current_user)
#fb_user = FbGraph::User.fetch(#user.uid).friends
puts #fb_user.inspect
The problem is on the #4 line - in this case I am getting error
OAuthException :: An access token is required to request this resource.
When I put there something like this:
#fb_user = FbGraph::User.fetch(request.env["omniauth.auth"].credentials.token).friends
I'll get
OAuthException :: (#803) Some of the aliases you requested do not exist: PRINTED OUT TOKEN
What's the proper way to obtain the access token?
EDIT: Current flow
class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController
def facebook
#user = User.find_for_facebook_oauth(request.env["omniauth.auth"], current_user)
#fb_user = FbGraph::User.fetch(request.env["omniauth.auth"].credentials.token).friends
if !#user
flash[:error] = 'This email address is already used in the system.'
redirect_to :back
elsif #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_registration_url
end
end
In User model:
def self.find_for_facebook_oauth(access_token, signed_in_resource=nil)
data = access_token.extra.raw_info
if user = User.where(:provider => 'facebook', :uid => data.id).first
user
elsif user = User.where('email = ? AND provider IS NULL', data.email).first
return false
else
...saving data...
end
return user if user
end
You can get an access token for test purposes via the Facebook Graph API Explorer. Make sure you select the proper fields that you want access to, and click "get access token". A more permanent solution is to register your app with Facebook so that you will be able to continually make requests without the token dying.
You should look into the Facebook OAuth dialogue.
I'm assuming you're trying to use the OAuth2 strategy instead of the Javascript SDK. Make sure you have set up a callback url like so:
client.redirect_uri = "http://your.client.com/facebook/callback"
In the controller that handles your callback, you should do something like this:
client.authorization_code = params[:code]
access_token = client.access_token! :client_auth_body
FbGraph::User.me(access_token).fetch
Make sure you've let fb_graph know what your app's id and secret are. You should look into this stackoverflow to keep your apps info safe.
I'll also plug the koala gem

How to get Koala to play nicely with Omniauth?

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

Resources