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.
Related
I'm using devise_token_auth for Email and Google Oauth2 based authentication(used omniauth-google-oauth2 gem for this). I've successfully managed to store sign in info of the user signing up/in through Google Oauth2 flow. The info includes:
{"auth_token"=>"token here", "client_id"=>"client id here", "uid"=>"uid here", "expiry"=>1620492005, "config"=>nil, "oauth_registration"=>true}
The flow for the above info was
Visit http://localhost:3000/auth/google_oauth2. This redirects you to the Google auth screen
User selects account and grants permission.
Oauth success callback from my app is executed at http://localhost:3000/auth/google_oauth2/callback
The code which executes for the first step is
module DeviseTokenAuth
class OmniauthCallbacksController < DeviseTokenAuth::ApplicationController
attr_reader :auth_params
before_action :validate_auth_origin_url_param
def omniauth_success
get_resource_from_auth_hash
set_token_on_resource
create_auth_params
if confirmable_enabled?
# don't send confirmation email!!!
#resource.skip_confirmation!
end
sign_in(:user, #resource, store: false, bypass: false)
#resource.save!
yield #resource if block_given?
render_data_or_redirect('deliverCredentials', #auth_params.as_json, #resource.as_json)
end
end
end
Problems I'm facing:
sign_in method call does not set #current_user despite that #resource and #auth_params have all the necessary info in them.
How can I inform my frontend app about the sign in info(token, client_id, uid)?
render_data_or_redirect('deliverCredentials', #auth_params.as_json, #resource.as_json)
this call does not redirect or render anything, instead it stays on the same page the URL it shows is http://localhost:3000/auth/google_oauth2/callback#
I basically have three questions now:
How can I use devise_token_auth to set the current_user based on the incoming auth headers?
I have added the following line to my controller, but still it fails to set #current_user
include DeviseTokenAuth::Concerns::SetUserByToken
maybe it is because I'm sending auth headers incorrectly? See my 3rd point below for this.
How am I supposed to send the sign in info to my frontend app?
Do I modify the above method somehow to send sign in info to my frontend app?
What and where do I put the auth headers in order to make authenticated requests?
When using devise_token_auth with email as auth provider I have to send 3 pieces to make an authenticated request i.e access-token, client_id and uid
Now in case of providers like Google/Facebook etc, do I set all of these headers or not?
I have used postman to test both of the following but it failed with Unauthorized error
Sent access-token, client_id and uid in headers
Sent Bearer my_token in authorization headers.
To get this working, we had to override the DeviseTokenAuth's OmniAuthCallbacks controller and update its render_data_or_redirect method.
The default definition of render_data_or_redirect is
def render_data_or_redirect(message, data, user_data = {})
if ['inAppBrowser', 'newWindow'].include?(omniauth_window_type)
render_data(message, user_data.merge(data))
elsif auth_origin_url
redirect_to DeviseTokenAuth::Url.generate(auth_origin_url, data.merge(blank: true))
else
fallback_render data[:error] || 'An error occurred'
end
end
The following routes.rb and custom_omniauth_callbacks_controller.rb have the changes needed to be done to get it working with j-toker library.
In the routes file
# config/routes.rb
mount_devise_token_auth_for 'User', at: 'auth', controllers: {
omniauth_callbacks: "devise_token_auth/custom_omniauth_callbacks"
}
and the definition of CustomOmniAuthCallbacksController was
# app/controllers/devise_token_auth/custom_omniauth_callbacks_controller.rb
module DeviseTokenAuth
class CustomOmniauthCallbacksController < DeviseTokenAuth::OmniauthCallbacksController
protected
def render_data_or_redirect(message, data, user_data = {})
if (['inAppBrowser', 'newWindow'].include?(omniauth_window_type) || auth_origin_url)
redirect_to DeviseTokenAuth::Url.generate(auth_origin_url, data.merge(blank: true))
else
fallback_render data[:error] || 'An error occurred'
end
end
end
end
now on the front-end side you need to configure j-toker
First, install j-toker package, yarn package or npm package
yarn add j-toker
or
npm i j-toker
then in your javascript application, configure the j-toker
import $ from "jquery";
$.auth.configure({
apiUrl: "https://your-api-domain.com",
emailSignInPath: "/auth/sign_in",
signOutPath: "/auth/sign_out",
emailRegistrationPath: "/auth",
tokenValidationPath: "/auth/validate_token"
authProviderPaths: {
facebook: "/auth/facebook",
google: "/auth/google_oauth2"
}
});
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
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
I have an app which connects to an iphone app, which in turn authenticates it's users via http_digest.
I'm using authlogic, and in my schema users of the website are "users" and users of the phone app are "people". So, i have user_sessions and people_sessions. To handle the http_digest auth, i'm using the authenticate_or_request_with_http_digest method like this:
def digest_authenticate_person
authenticate_or_request_with_http_digest do |email, password|
#ldb is just a logging method i have
ldb "email = #{email.inspect}, password = #{password.inspect}"
person = Person.find_by_email(email)
if person
ldb "Authentication successful: Got person with id #{person.id}"
#current_person_session = PersonSession.create(person)
else
ldb "Authentication failed"
#current_person_session = nil
end
return #current_person_session
end
end
I can see in the logs that password is nil: only email is passed through to the inside of the authenticate_or_request_with_http_digest block.
Im testing this with a curl call like so:
curl --digest --user fakename#madeup.xyz:apass "http://localhost:3000/reports.xml"
I'd expect "fakename#madeup.xyz" and "apass" to get passed through to the inside of the block. Once i have the password then i can use a combination of email and password to find (or not) a user, in the normal way. Does anyone know how i can get access to the password as well?
grateful for any advice - max
EDIT - on further googling, i think i'm using this method wrong: i'm supposed to just return the password, or the crypted password. But then how do i compare that against the password passed as part of the http_digest username?
Found the answer: i had a fundamental misunderstanding of how authenticate_or_request_with_http_digest works: after reading the documentation (in the source code of the gem) i realised that the purpose of this method is not to do the authentication, its purpose is to provide the "email:realm:password" string to the browser, let the browser encrypt it, and check the result against it's own calculated (or cached) version of this.
Here's how i set it up:
def current_person
if #current_person
#current_person
else
load_current_person
end
end
#use in before_filter for methods that require an authenticated person (mobile app user)
def require_person
unless current_person
redirect_to root_path
end
end
def load_current_person
#check user agent to see if we're getting the request from the mobile app
if request.env['HTTP_USER_AGENT'] =~ /MobileAppName/
result = digest_authenticate_person
if result == 401
return 401
elsif result == true
#make authlogic session for person
#current_person_session = PersonSession.new(#person_from_digest_auth)
#current_person = #person_from_digest_auth
end
end
end
#this method returns either true or 401
def digest_authenticate_person
authenticate_or_request_with_http_digest(Person::DIGEST_REALM) do |email|
person = Person.find_by_email(email)
#result = nil
if person
#need to send back ha1_password for digest_auth, but also hang on to the person in case we *do* auth them successfully
#person_from_digest_auth = person
#result = person.ha1_password
else
#person_from_digest_auth = nil
#result = false
end
#result
end
end
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