Rails - Facebook with Omniauth and Koala: How to renew an expired token - ruby-on-rails

I have an application where users can link their Facebook accounts. They can log in using their email, but they can link their Facebook account.
In the view where I show the linked social networks (Facebook and others), I have something like this:
<%= image_tag #facebook.get_facebook_picture %>
This will call an instance method like this:
def get_facebook_picture
unless self.token.nil?
facebook_graph = Koala::Facebook::GraphAPI.new(self.token)
fb_picture = facebook_graph.get_picture("me", { :type => "small" })
end
end
This will work well unless the Facebook token that I have stored in my DB is expired. So I have added this exception handler in the mentioned controller:
def facebook_exception_handler exception
if exception.fb_error_type.eql? 'OAuthException'
# Let's get a new auth token... How?
else
logger.debug "Damn it. We don't know what error is coming from FB"
raise exception
end
end
I catch the exception correctly, but I fail to see how would I renew the access token that I have in my database. Notice that the access token that I have has been inserted using OmniAuth. So my question is:
Given that I have an OAuthException, how can I renew the access token of a particular user (UID) using Omniauth?

The simple case is that you re-auth the user with FB, exactly as you authorized them in the first place. To get the token in the first place, i'm assuming you're using omniauth (and onmiauth-facebook) to authenticate against FB. That means you've got a route and a controller action to handle the auth callback, and a function that inserts the token into the db.
The access token you originally got with omniauth can become invalid for various reasons - expiry, or because the user changed their FB password, and possibly others. In those cases, another OAuth call will return a valid token. Just call again (as you did when you first authorized the user) and replace the invalid token with the new one, in your DB, and you're good.
This gist (my own answer to a related question i asked here) has some code covering that, but it sounds like you've already got this covered. Save enough state to then re-attempt whatever triggered the exception and you're good.
It's also possible that the token is now invalid because the user has changed their FB app settings to de-authorize your app. In that case, the user will see the FB permissions dialog as if they were a new user authenticating against FB for the first time. (FB)
Does that make sense?

You can change the RailsCasts koala tutorial connection with this:
def facebook
if self.facebook_expires_at < Time.now
oauth = Koala::Facebook::OAuth.new(ENV["FACEBOOK_KEY"], ENV["FACEBOOK_SECRET"])
new_access_info = oauth.exchange_access_token_info self.facebook_token
new_access_token = new_access_info["access_token"]
new_access_expires_at = DateTime.now + new_access_info["expires"].to_i.seconds
self.update_attributes!(:facebook_token => new_access_token,
:facebook_expires_at => new_access_expires_at )
end
#facebook ||= Koala::Facebook::API.new(self.facebook_token)
block_given? ? yield(#facebook) : #facebook
rescue Koala::Facebook::APIError => e
logger.info e.to_s
nil
end

Related

Implementing social login for Rails Api-only app

I'm struggling to find a working method to implement oauth2 login via Facebook & Google for my existing api-only rails app. Login flow & jwt management is done with Devise & Doorkeeper, following this guide.
I tried with Doorkeeper-grants-assertion examples, but none of them is working.
The problem i have is that i can't exchange the provider's token with my jwt token.
Client side (Android and iOS apps) i can login with provider and get the token, but when i try to authorize the user to create a new token, it gives me errors.
The code is the same as examples. In the case of Google i'm skipping token request because i can already get it from client:
class GoogleController
def initialize(auth_code)
#auth_code = auth_code
#user_data = user_data
end
def user_data
url = "https://www.googleapis.com/oauth2/v3/userinfo?access_token=" + #auth_code
response = Faraday.get(url, {:Accept => 'application/json'})
#resp = JSON.parse(response.body)
end
def email
#resp['email']
end
def first_name
#resp['first_name']
end
def last_name
#resp['last_name']
end
def get_user!
# below you should implement the logic to find/create a user in your app basing on #user_data
# It should return a user object
user = User.find_by(email: email)
if user
Rails.logger.info "User"
user
else
user = User.new(email: email, password: Devise.friendly_token.first(10))
user.save
Rails.logger.info "No User"
user
end
end
end
I'm using postman to make requests, below there is the response if my body is:
{
"client_id": "doorkeeper_app_uid",
"client_secret": "doorkeeper_app_secret",
"grant_type": "assertion",
"provider": "google",
"assertion": "MY USER TOKEN" }
{ "error": "invalid_client",
"error_description": "Client authentication failed due to unknown client, no client authentication included, or unsupported authentication method." }
I just found out i didn't return an User object, that's why Facebook didn't work.
Now, with the same code, only different token endpoint, Facebook login is working and i can find or create the user and return the jwt token, while Google is not.
If someone could point me in the right direction it would be great.
EDIT
after further investigation i'm at this point:
i can find or create my Google authenticated user, but when i return it to doorkeeper assert grant extension, it fails validation
def validate_resource_owner
!resource_owner.nil?
end
in class
PasswordAccessTokenRequest
and i can't generate new jwt token.
What's different from facebook that makes this validation to fail?
Incredible guys, mystical things happens but i've found a solution.
Somehow there was a conflict with 2 providers in doorkeeper.rb initializer if written like so: (Don't do this)
resource_owner_from_assertion do
if provider == "facebook"
g = Api::V1::FacebookController.new(params[:assertion])
g.get_user!
end
if provider == "google"
g = Api::V1::GoogleController.new(params[:assertion])
return g.get_user!
end
end
instead do something like:
resource_owner_from_assertion do
provider = params[:provider]
controller = (provider == "facebook") ? Api::V1::FacebookController.new(params[:assertion]) : Api::V1::GoogleController.new(params[:assertion])
controller.get_user!
end
Then there was another issue inside controllers, because i used "user" as variable name:
user = User.find_by(email: email)
and this is apparently bad, so use
facebook_user = User.find_by(email: email)
Now everything seems to work as it is supposed to. I hope someone will find this useful.

Omniauth-Facebook giving error in callback - Email can't be blank?

Hi my devise login appears to work, but for some reason Facebook is not sending me the email in my API request. Then I am getting this error:
I read up on the July 18th, 2015 adjustment and added scope. No luck. Here is the initializer:
config.omniauth :facebook, ENV['FACEBOOK_APP_KEY'],
ENV['FACEBOOK_APP_SECRET'], scope: 'email', info_fields:'email,name'
Maybe I'm missing something on the FB Developer Page?
You will have to handle it :
Some Facebook users have unverified emails, so Facebook will not give it to you
There is Facebook users with only telephone numbers and no email
Facebook allow the user to not provide some information to your app, for example the email
It happens with profiles with unverified emails and when user choose not to provide your app with their email
rescue that error and redirect user to normal sign up page so they continue with providing their email address
Ok, if what Nicolas is saying is correct then you can easily solve this problem. Before save populate some fake and unique email with "+":
Add to your User model something like this:
class User < ActiveRecord::Base
before_validation :adjust_email, on: :create
private
def adjust_email
self.email = "fake+#{generate_token}" if email.blank?
true
end
def generate_token
rand(36**8).to_s(36)
end
end
And you will have to show your user this fake email, so that she will be able to login with it later. A downside of this solution is that you will not be able to send emails to such users, but maybe you don't want to.
What Remon suggested is also a valid option to redirect to sign up in case you get this particular error.
So, it all depends on what you actually plan to do with the email.

How can I connect Aweber to my Rails app using OAuth?

I'm trying to integrate my Rails app with Aweber via OAuth, using the official aweber gem.
If I follow their flow in the Rails console, I can get an access token, no problems:
oauth = AWeber::OAuth.new(ENV["AWEBER_CONSUMER_KEY"], ENV["AWEBER_CONSUMER_SECRET"])
puts oauth.request_token.authorize_url
# => https://auth.aweber.com/1.0/oauth/authorize?oauth_token=xxxxxxxxxxxxxx
Then I visit that URL, type in my credentials, get a verification code, and go back to the rails console:
oauth.authorize_with_verifier 'xxxxxx'
# => #<OAuth::AccessToken>
Success!
The problem is, I want to do this in the real world, not just at the console, which means my Ruby code needs to be broken up into two separate actions. First, there's the controller action which redirects to Aweber's Oauth page:
def aweber
oauth = AWeber::OAuth.new(ENV["AWEBER_CONSUMER_KEY"], ENV["AWEBER_CONSUMER_SECRET"])
redirect_to oauth.request_token(oauth_callback: "http://127.0.0.1:3000/auth/aweber/callback").authorize_url
end
Then there's the action which gets the access token after the user has input their credentials and been redirected:
def aweber_callback
oauth = AWeber::OAuth.new(ENV["AWEBER_CONSUMER_KEY"], ENV["AWEBER_CONSUMER_SECRET"])
oauth.authorize_with_verifier(params[:oauth_verifier])
end
When I do it this way, the final line (authorize_with_verifier) always raises #<OAuth::Unauthorized: 401 Unauthorized>.
Seems like the problem is that I'm initializing the oauth variable twice, meaning I have two unrelated instances of AWeber::Oauth ... and only the instance of AWeber::Oauth that generated the authorize_url can get the access token. But I can't get the same instance in both aweber_callback and aweber because I'm dealing with two completely different threads and instances of the controller.
When I inspect oauth, I can see that the internal variables oauth.request_token.params["oauth_token"] and oauth.request_token.params["oauth_token_secret"] are different in each oauth, which I'm guessing is the cause of the problem. I can get the 'correct' oauth_token from the params (params[:oauth_token]), but I can't figure out how to get the correct oauth_token_secret (not to mention that manually setting instance variables like this feels very hacky and is probably not the best approach.)
How can I generate an access token?
I finally got this working by storing the oauth_token_secret in the session. (And I have to say, I'm very unimpressed by Aweber's documentation and API setup. This took 10 times longer than it should have.)
Gemfile
gem 'aweber', '~> 1.6.1', require: "aweber"
Routes
get "auth/aweber", to: "integrations#aweber", as: :aweber
get "auth/aweber/callback", to: "integrations#aweber_callback", as: :aweber_callback
Integrations Controller
def aweber
oauth = get_aweber_oauth
request_token = oauth.request_token(oauth_callback: aweber_redirect_uri)
session[:aweber_oauth_token_secret] = request_token.secret
redirect_to request_token.authorize_url
end
def aweber_callback
oauth = get_aweber_oauth
oauth.request_token = OAuth::RequestToken.from_hash(
oauth.consumer,
oauth_token: params[:oauth_token],
oauth_token_secret: session[:aweber_oauth_token_secret],
)
access_token = oauth.authorize_with_verifier(params[:oauth_verifier])
# TODO save access_token.token and access_token.secret
end
private
def get_aweber_oauth
AWeber::OAuth.new(ENV["AWEBER_CONSUMER_KEY"], ENV["AWEBER_CONSUMER_SECRET"])
end
def aweber_redirect_uri
#_aweber_callback_uri ||= begin
if Rails.env.production?
redirect_host = "http://myproductionurl.com"
else
redirect_host = "http://127.0.0.1:3000"
end
"#{redirect_host}#{Rails.application.routes.url_helpers.aweber_callback_path}"
end
end
The next step is to store access_token.token and .secret in my DB,
then I'll be able to authorize users on future requests like this:
oauth = AWeber::OAuth.new(ENV["AWEBER_CONSUMER_KEY"], ENV["AWEBER_CONSUMER_SECRET"])
oauth.authorize_with_access(current_user.aweber_token, current_user.aweber_secret)
aweber = AWeber::Base.new(oauth)
# Make calls using "aweber"...
I tried using the gem omniauth-aweber in combination with the omniauth gem, but I couldn't get it working (which is a shame, because I'm using other omniauth-xxx gems in this app and it would have been nice to keep things consistent.) Basically, that gem automatically handles the /auth/aweber part of the process, but after it redirects me back to /auth/aweber/callback/ I can't see any way to get the oauth_token_secret - it's not in the request params, the session, or the cookies.
I've answered my own question now but I'll give the bounty to anyone who can come up with an obvious improvement on the above, or figure out a way to make it all work with omniauth-aweber.
Reading through the AWeber API Ruby Library, this bit stands out
What if I don’t want to verify every time?
After verifying once, the oauth object contains an
oauth.access_token.token and and oauth.access_token.secret which may
be used to authorize your application without having to verify via
url:
... oauth.authorize_with_verifier('verification_code') puts 'Access
token: ' + oauth.access_token.token puts 'Access token secret: ' +
oauth.access_token.secret The token and secret can then be saved, and
authorization can be performed as follows:
require 'aweber'
oauth = AWeber::OAuth.new('consumer_key', 'consumer_secret')
#Rather than authorizing with the verification code, we use the token and secret
oauth.authorize_with_access(YOUR_ACCESS_TOKEN, YOUR_ACCESS_TOKEN_SECRET)
aweber = AWeber::Base.new(oauth)
So let's run through this:
You can create a class that keeps an object in memory for each User for enough time to finish the sign in and then save the token and secret for use until they expire.
Please note current_user is meant to be anything that uniquely identifies the user. You could use the session ID if your users aren't logged in yet at this point
class AWeberSignIn
def self.start_signing user
oauth = Rails.cache.fetch("#{user}/aweber", expires_in: 5.minutes) do
AWeber::OAuth.new(ENV["AWEBER_CONSUMER_KEY"], ENV["AWEBER_CONSUMER_SECRET"])
end
oauth.request_token(oauth_callback: "http://127.0.0.1:3000/auth/aweber/callback").authorize_url
end
def self.authorize_with_verifier user, oauth_verifier
oauth = Rails.cache.fetch("#{user}/aweber")
oauth.authorize_with_verifier(oauth_verifier)
[oauth.access_token.token, oauth.access_token.secret]
end
def self.get_base_from_token token, secret
oauth = AWeber::OAuth.new(ENV["AWEBER_CONSUMER_KEY"], ENV["AWEBER_CONSUMER_SECRET"])
oauth.authorize_with_access(token, secret)
AWeber::Base.new(oauth)
end
end
With this class, your controller methods become:
def aweber
redirect_to AWeberSignIn.start_signin current_user #Assuming you have a current_user helper. Use whatever gives you a unique value per user
end
def aweber_callback
token, secret = AWeberSignIn.authorize_with_verifier(current_user, params[:oauth_verifier])
#Do something with token and secret. Maybe save it to User attributes?
#You can then use them to get a AWeber base object via AWeberSignIn.get_base_from_token token, secret
end
Please note that this is using low-level Rails caching. Make sure you set up your caching technique if you want something different from the default

Omniauth facebook sessions controller create action integration after Michael Hartl tutorial

I have just finished the Michael Hartl tutorial. I am trying to implement omniauth-facebook to sign-up and sign-in users. I am having trouble creating the master variable to use in the create action in the sessions controller. My assumption is that I should put in an if else statement to see if the master is either signing on through facebook or through the default form? Or should I use and or statement:
(master = Master.find_by(email: params[:session][:email].downcase) || Master.from_omniauth(env["omniauth.auth"])) ?
Here is the omniauth-facebook sessons controller create action code:
def create
user = User.from_omniauth(env["omniauth.auth"])
session[:user_id] = user.id
redirect_to root_url
end
And here is my current sessions controller create action code:
class SessionsController < ApplicationController
def new
end
def create
master = Master.find_by(email: params[:session][:email].downcase)
if master && master.authenticate(params[:session][:password])
sign_in master
redirect_back_or master
else
flash.now[:error] = 'Invalid email/password combination'
render 'new'
end
end
.
.
.
end
Whilst I might be late by one year to answer this question, I still think that new viewers with the same sort of problem like you had a year ago might benefit from the following answer
As I understand it the actual question is:
How does one integrate Facebook Authentication with an Authentication system that is built from Scratch?
Since you said you used Michael Hartl's tutorial I went ahead and adapted my answer to an authentication system written by Michael himself. Now let's go over each piece of code one step at a time.
1
First of all we need to create two additional columns to our existing Users table for the provider and uid as follows...
rails g migration add_facebook_auth_to_users provider uid
...and add omniauth-facebook gem to the Gemfile...
Gemfile
gem 'omniauth-facebook', '2.0.1'
Don't forget to run bundle install and migrate your database after.
2
Next we would need to go ahead and edit our Sessions controller...
sessions_controller.rb
def create
user = User.find_by(email: params[:session][:email].downcase)
if user && user.authenticate(params[:session][:password])
if user.activated?
log_in user
params[:session][:remember_me] == '1' ? remember(user) : forget(user)
redirect_back_or user
else
message = "Account not activated. "
message += "Check your email for the activation link."
flash[:warning] = message
redirect_to root_url
end
else
flash.now[:danger] = 'Invalid email/password combination'
render 'new'
end
end
def create_facebook
user = User.from_omniauth(env["omniauth.auth"])
log_in user
redirect_back_or user
end
Sessions controller above contains two create actions. First one is the untouched method taken straight from the book, when the second one is a piece of code I used specifically for Facebook authentications.
Line user = User.from_omniauth(env["omniauth.auth"]) has two purposes. Create a user IF his/her unique facebook uid is not in the database ELSE log this person in the app if the uid exists. More on this later.
3
Next lets create a working .from_omniauth method that you briefly showed inside of your code in your question...
user.rb
def self.from_omniauth(auth)
where(provider: auth.provider, uid: auth.uid).first_or_initialize.tap do |user|
user.provider = auth.provider
user.uid = auth.uid
user.name = auth.info.name unless user.name != nil
user.email = SecureRandom.hex + '#example.com' unless user.email != nil
user.activated = true
user.password = SecureRandom.urlsafe_base64 unless user.password != nil
user.save!
end
end
Here we try to find a user that matches the given provider and uid values from the hash and then we call first_or_initialize on the result of this. This method will either return the first matching record or initialize a new record with the parameters that were passed in. We then call tap on this to pass that User instance to the block. Inside the block we set various attributes on the user based on values from the OmniAuth hash.
You might be interested as to why did I put the unless statement after some of the attribute initializations. Well consider the situation whereby the user wants to update the profile, like change the name or smth, and then logs out. When that user finally decides to log back in, the .from_omniauth method will overwrite the User's in question update to the original facebook values, unless we stop it from doing so.
Also notice the use of SecureRandom library. Inside of the traditional authentication that was used by Michael Hartl inside his book, he introduces validations to email and password submissions. Emails must be neither blank nor taken. Likewise, a password has to be greater than 6 characters in length. Since emails have to be present and unique I decided to create dummy emails using SecureRandom.hex + #example.com. This will create a random hexadecimal string, like 52750b30ffbc7de3b362, and append it to #example.com, hence generating the unique dummy email. Same goes for the password, however, I preferred to generate a random base64 string using SecureRandom.urlsafe_base64. The most important thing to remember that Facebook users don't need to know this information to login since that is the whole point of using Facebook authentication. This allows them to add this information with real data later on if they desire to do so.
4
Now would be a good time to add the button on the main application page for users to let them actually log in into the app...
_header.html.erb
<% if logged_in? %>
.
.
.
<% else %>
.
.
<li><%= link_to "Sign in with Facebook", "/auth/facebook", id: "sign_in" %></li>
<% end %>
5
So, now we can send a request to facebook. One problem remains that we cannot actually handle the callback. To do so we need to do the following...
Add get 'auth/:provider/callback' => 'sessions#create_facebook' to the routes.rb file
Use convenient Javascript SDK provided by Facebook. Let's create a custom javascript file that uses this SDK...
app/assets/javascripts/facebook.js.erb
jQuery(function() {
$('body').prepend('<div id="fb-root"></div>');
return $.ajax({
url: window.location.protocol + "//connect.facebook.net/en_US/sdk.js",
dataType: 'script',
cache: true
});
});
window.fbAsyncInit = function() {
FB.init({
appId: 'your_facebook_app_id_goes_here',
version: 'v2.3',
cookie: true
});
$('#sign_in').click(function(e) {
e.preventDefault();
return FB.login(function(response) {
if (response.authResponse) {
return window.location = '/auth/facebook/callback';
}
});
});
return $('#sign_out').click(function(e) {
FB.getLoginStatus(function(response) {
if (response.authResponse) {
return FB.logout();
}
});
return true;
});
};
6
And that is it, except for one small caveat...
If user decides to go and edit the profile, he/she would see our dummy email displayed right in the middle.This happends because Rails is smart enough to autofill User's information.In this case this, frankly, is quite embarassing. Good thing, however, that it is an easy fix. Just set the value to nil inside the form, like so...
<%= f.email_field :email, value: nil, class: 'form-control' %>
Hopefully this helps people who would like to go the route of building authentication systems from scratch :)
Ok I was trying to integrate omniauth with the custom authorization code from the tutorial. After some research I decided to remove all the tutorial authorization code and go with Devise instead.
Timur's answer is excellent, but assumes that the OP has already configured some things for OmniAuth. Unfortunately some readers will not have done the config and will get errors.
The minimum config for omniauth is to set up facebook credientials, make them available to the app and create a file called config/initializers/omniauth.rb with the following:
Rails.application.config.middleware.use OmniAuth::Builder do
provider :developer unless Rails.env.production?
provider :facebook, ENV['FACEBOOK_KEY'], ENV['FACEBOOK_SECRET']
end
And of course, it would be wise to write some tests for this feature.
Neither this nor Timur's answer deals with the situation where a user has created a local login account, then tries to log in from Facebook. It will fail due to a duplicate email or create a new account if the Facebook email is different. the User.from_omniauth(auth) function should be updated to handle this case. omniauth has a page about how to handle this, but will require significant rework from Miachal's hartl's tutorial: https://github.com/omniauth/omniauth/wiki/Managing-Multiple-Providers

How do I fetch the access token for a user using the Twitter gem and the Omniauth gem when the users is already logged in?

Currently I authenticate the user using omniauth. This looks like this in my sessions controller and works well:
def create
auth = request.env['omniauth.auth']
unless #auth = Authentication.find_from_hash(auth)
# Create a new user or add an auth to existing user, depending on
# whether there is already a user signed in.
#auth = Authentication.create_from_hash(auth, current_user)
end
# Log the authorizing user in.
self.current_user = #auth.user
redirect_to authentications_url, :notice => "You've signed in!"
end
After this, I've stored the twitter uid in my authentications table (I also use linkedin, facebook) and I think that the twitter sessions has been closed.
How do I now authenticate so that I can use the Twitter gem? I think it should be something like this if I was calling it right after the omniauth callback.
token = auth['credentials']['token'],
secret = auth['credentials']['secret']
Twitter.oauth_token = token
Twitter.oauth_token_secret = secret
I clearly need to restart the session and put the token and secret in the right place. How can I create a method to do this?
You need to store both the token and the secret provided by Twitter in your authentications table (Authentication.create_from_hash). As long as you're doing that, this should work:
twitter_credentials = current_user.authorizations.find_by_provider(:twitter)
Twitter.oauth_token = twitter_credentials.token
Twitter.oauth_token_secret = twitter_credentials.token_secret
That's assuming that in your authentications table you store the Twitter token and secret as token and token_secret, as well as storing the provider as twitter

Resources