I have successfully integrated the OmniAuth Facebook login flow into my rails application on the server side. However, I am also trying to get this to work using the Facebook Javascript SDK on the client side and am running into some issues.
EDIT: THIS ISSUE ONLY SEEMS TO BE HAPPENING IN CHROME AND NOT IN SAFARI OR FIREFOX
Sessions Controller - This works on the server side flow
def create
auth = request.env['omniauth.auth']
#if an authorization does not exisit, it will create a new authorization record. it will also create a new user record if a user is not currently logged in
unless #auth = Authorization.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 = Authorization.create_from_hash(auth, current_user)
#add the friends array to user record. as of now only doing this on the initial user create
#friends = []
FbGraph::User.me(#auth.user.authorization.facebook_token).fetch.friends.each do |t|
#friends << t.identifier
end
u = #auth.user
u.facebook_friends = #friends
u.save
end
#store a new auth token if needed (if the new token in the hash does not match the one stored in the database for authorization)
Authorization.check_if_new_auth_token_is_needed(auth)
# Log the authorizing user in.
self.current_user = #auth.user
redirect_to root_url
end
If I simply hit the /auth/facebook path, the user will be logged in
Routes
match '/auth/:provider/callback', :to => 'sessions#create'
Now on the homepage view I am trying to now run a client side flow login
Homepage View
<script>
$(function() {
$('a').click(function(e) {
e.preventDefault();
FB.login(function(response) {
if (response.authResponse) {
$('#connect').html('Connected! Hitting OmniAuth callback (GET /auth/facebook/callback)...');
// since we have cookies enabled, this request will allow omniauth to parse
// out the auth code from the signed request in the fbsr_XXX cookie
$.getJSON('/auth/facebook/callback', function(json) {
$('#connect').html('Connected! Callback complete.');
$('#results').html(JSON.stringify(json));
});
}
}, { scope: 'email,publish_stream' });
});
});
</script>
<p id="connect">
Connect to FB
</p>
<p id="results" />
I'm getting the following error in my log
{"error":{"message":"Missing authorization
code","type":"OAuthException","code":1}}
Basically, Omniauth is not picking up on the facebook signed request from the FB.login action (as https://github.com/mkdynamic/omniauth-facebook/blob/master/example/config.ru says it's supposed to).
Any ideas on how I can get this to work properly or what I may be doing incorrectly?
I realize this question is a year old, but I've run into this problem twice now so hopefully this helps someone.
There are two github threads related to this problem:
https://github.com/mkdynamic/omniauth-facebook/issues/73
and
https://github.com/intridea/omniauth-oauth2/issues/31
The source of the problem is the callback_phase method inside the omniauth-oauth2 gem:
if !options.provider_ignores_state && (request.params['state'].to_s.empty? || request.params['state'] != session.delete('omniauth.state'))
raise CallbackError.new(nil, :csrf_detected)
end
request.params['state'] and session['omniauth.state'] are both nil so the condition fails and a CallbackError exception is raised.
One solution is to set provider_ignores_state to true which circumvents the condition:
config.omniauth :facebook, ENV['FACEBOOK_APP_ID'], ENV['FACEBOOK_APP_SECRET'], {
strategy_class: OmniAuth::Strategies::Facebook,
provider_ignores_state: true,
}
As pointed out in the threads above it's not a permanent solution since it can leave you open to csrf attacks.
One other thing to note is that Chrome has issues writing cookies to localhost. Try using lvh.me as your domain (it resolves to 127.0.0.1).
More code to patch the problem isn't usually a great path, but if neither of those solutions work then you can always create your own handler and parse the Facebook cookies:
def handle_facebook_connect
#provider = 'facebook'
#oauth = Koala::Facebook::OAuth.new(ENV["FACEBOOK_ID"], ENV["FACEBOOK_SECRET"])
auth = #oauth.get_user_info_from_cookies(cookies)
# Get an extended access token
new_auth = #oauth.exchange_access_token_info(auth['access_token'])
auth['access_token'] = new_auth["access_token"]
# Use the auth object to setup or recover your user. The following is
# and example of how you might handle the response
if authentication = Authentication.where(:uid => auth['user_id'], :provider => #provider).first
user = authentication.user
sign_in(user, :event => :authentication)
end
# Redirect or respond with json
respond_to do |format|
format.html { redirect_to user }
format.json { render json: user }
end
end
Then you'll need to redirect to the 'handle_facebook_connect' method when you receive a connected response:
FB.Event.subscribe('auth.authResponseChange', function(response) {
if(response.status === 'connected'){
if(response.authResponse){
// Redirect to our new handler
window.location = '/handle_facebook_connect';
// Or make an ajax request as in the code in the original question:
// $.getJSON('/handle_facebook_connect', function(json) {
// $('#connect').html('Connected! Callback complete.');
// $('#results').html(JSON.stringify(json));
// });
}
} else if (response.status === 'not_authorized'){
Facebook.message(Facebook.authorize_message);
} else {
FB.login();
}
});
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 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.
How can I create a dynamic QR code on a rails app such that the moment it is scanned and successfully processed, the open page bearing the QR code can then just redirect to the success page.
This is similar to the whatsapp web implementation where the moment the android app scans the QR code, the page loads the messages.
Am more interested in is the management of the sessions. When the QR is scanned am able to reload the page where it was displayed and then redirect to another page. any idea?
You could update the User model to be able to store an unique token value to use in you QR Codes; e.g.
$ rails generate migration add_token_to_user token:string
Or a separate related model
$ rails generate model Token value:string user:belongs_to
Then generate unique Token value that can be used within an URL and encode it
into a QRCode
# Gemfile
gem "rqrcode"
# app/models/token.rb
require "securerandom"
class User < ActiveRecord::Base
def generate_token
begin
self.token = SecureRandom.urlsafe_base64 #=> "b4GOKm4pOYU_-BOXcrUGDg"
end while self.class.exists?(token: token)
end
def qr_code
RQRCode::QRCode.new(
Rails.application.routes.url_helpers.url_for(
controller: "session",
action: "create",
email: email,
token: token
)
)
end
end
Then display this QRCode somewhere in your application
# app/views/somewhere.html.erb
<%= #token.qr_code.as_html %>
Then wire up your application's routes and controllers to process that generated
and encoded QRCode URL
# config/routes.rb
Rails.application.routes.draw do
# ...
get "/login", to: "sessions#new"
end
# app/controller/sessions_controller.rb
class SessionsController < ApplicationController
def create
user = User.find_by(email: params[:email], token: params[:token])
if user
session[:user_id] = user.id # login user
user.update(token: nil) # nullify token, so it cannot be reused
redirect_to user
else
redirect_to root_path
end
end
end
References:
whomwah/rqrcode: A Ruby library that encodes QR Codes
Module: SecureRandom (Ruby 2_2_1)
#352 Securing an API - RailsCasts
I am adding a new answer for two reasons:
1. Acacia repharse the question with an emphasis on What's App redirection of
the page with the QR Code being view, which I did not address in my initial
solution due a misunderstanding of the problem, and
2. Some people have found the first answer helpful and this new answer would
change it significantly that whilst similar, but no longer the same
When the QR is scanned am able to reload the page where it was displayed and
then redirect to another page
-- Acacia
In order to achieve this there requires to some kind of open connection on the
page that is displaying the QRCode that something interpretting said QRCode can
use to effect it. However, because of the application you trying to mimic
requires that only that one User viewing the page is effected, whilst not
actually being logged in yet, would require something in the page to be unique.
For the solution to this problem you will need a couple of things:
An unique token to identify the not logged-in User can use to be contacted /
influenced by an external browser
A way of logging in using JavaScript, in order to update the viewed page to
be logged after previous step's event
Some kind of authentication Token that can be exchange between the
application and the external QRCode scanner application, in order to
authentication themselves as a specific User
The following solution stubs out the above 3rd step since this is to
demonstrate the idea and is primarily focused on the server-side of the
application. That being said, the solution to the 3rd step should be as simple
as passing the know User authentication token by appending it to the URL within
the QRCode as an additional paramater (and submitting it as a POST request,
rather than as a GET request in this demonstration).
You will need some random Tokens to use to authentication the User with and
exchange via URL embedded within the QCcode; e.g.
$ rails generate model Token type:string value:string user:belongs_to
type is a reserverd keyword within Rails, used for Single Table Inheritance.
It will be used to specific different kinds of / specialized Tokens within this
application.
To generate unique Token value that can be used within an URL and encode it
into a QRCode, use something like the following model(s) and code:
# Gemfile
gem "rqrcode" # QRCode generation
# app/models/token.rb
require "securerandom" # used for random token value generation
class Token < ApplicationRecord
before_create :generate_token_value
belongs_to :user
def generate_token_value
begin
self.value = SecureRandom.urlsafe_base64 #=> "b4GOKm4pOYU_-BOXcrUGDg"
end while self.class.exists?(value: value)
end
def qr_code(room_id)
RQRCode::QRCode.new(consume_url(room_id))
end
def consume_url(room_id)
Rails.application.routes.url_helpers.url_for(
host: "localhost:3000",
controller: "tokens",
action: "consume",
user_token: value,
room_id: room_id
)
end
end
# app/models/external_token.rb
class ExternalToken < Token; end
# app/models/internal_token.rb
class InternalToken < Token; end
InternalTokens will be only used within the application itself, and are
short-lived
ExternalTokens will be only used to interact with the application from
outside; like your purposed mobile QRCode scanner application; where the User
has either previously registered themselves or has logged in to allow for
this authentication token to be generated and stored within the external app
Then display this QRCode somewhere in your application
# e.g. app/views/tokens/show.html.erb
<%= #external_token.qr_code(#room_id).as_html.html_safe %>
I also hide the current #room_id within the <head> tags of the application
using the following:
# e.g. app/views/tokens/show.html.erb
<%= content_for :head, #room_id.html_safe %>
# app/views/layouts/application.html.erb
<!DOCTYPE html>
<html>
<head>
<title>QrcodeApp</title>
<!-- ... -->
<%= tag("meta", name: "room-id", content: content_for(:head)) %>
<!-- ... -->
</head>
<body>
<%= yield %>
</body>
</html>
Then wire up your application's routes and controllers to process that generated
and encoded QRCode URL.
For Routes we need:
Route to present the QRCode tokens; "token#show"
Route to consume / process the QRCode tokens; "token#consume"
Route to log the User in with, over AJAX; "sessions#create"
We will also need some way of opening a connection within the display Token page
that can be interacted with to force it to login, for that we will need:
mount ActionCable.server => "/cable"
This will require Rails 5 and ActionCable to implment, otherwise another
Pub/Sub solution; like Faye; will need to be used instead with older versions.
All together the routes look kind of like this:
# config/routes.rb
Rails.application.routes.draw do
# ...
# Serve websocket cable requests in-process
mount ActionCable.server => "/cable"
get "/token-login", to: "tokens#consume"
post "/login", to: "sessions#create"
get "/logout", to: "sessions#destroy"
get "welcome", to: "welcome#show"
root "tokens#show"
end
Then Controllers for those actions are as follows:
# app/controller/tokens_controller.rb
class TokensController < ApplicationController
def show
# Ignore this, its just randomly, grabbing an User for their Token. You
# would handle this in the mobile application the User is logged into
session[:user_id] = User.all.sample.id
#user = User.find(session[:user_id])
# #user_token = Token.create(type: "ExternalToken", user: #user)
#user_token = ExternalToken.create(user: #user)
# keep this line
#room_id = SecureRandom.urlsafe_base64
end
def consume
room_id = params[:room_id]
user_token = params[:user_token] # This will come from the Mobile App
if user_token && room_id
# user = Token.find_by(type: "ExternalToken", value: user_token).user
# password_token = Token.create(type: "InternalToken", user_id: user.id)
user = ExternalToken.find_by(value: user_token).user
password_token = InternalToken.create(user: user)
# The `user.password_token` is another random token that only the
# application knows about and will be re-submitted back to the application
# to confirm the login for that user in the open room session
ActionCable.server.broadcast("token_logins_#{room_id}",
user_email: user.email,
user_password_token: password_token.value)
head :ok
else
redirect_to "tokens#show"
end
end
end
The Tokens Controller show action primarily generates the #room_id value for
reuse in the view templates. The rest of the code in the show is just used to
demonstrate this kind of application.
The Tokens Controller consume action requires a room_id and user_token to
proceed, otherwise redirects the User back to QRCode sign in page. When they are
provided it then generates an InternalToken that is associated with the User
of the ExternalToken that it will then use to push a notification / event to
all rooms with said room_id (where there is only one that is unique to the
User viewing the QRCode page that generate this URL) whilst providing the
necessary authentication information for a User (or in this case our
application) to log into the application without a password, by quickly
generating an InternalToken to use instead.
You could also pass in the User e-mail as param if the external application
knows about it, rather than assuming its correct in this demonstration example.
For the Sessions Controller, as follows:
# app/controller/sessions_controller.rb
class SessionsController < ApplicationController
def create
user = User.find_by(email: params[:user_email])
internal_token = InternalToken.find_by(value: params[:user_password_token])
# Token.find_by(type: "InternalToken", value: params[:user_password_token])
if internal_token.user == user
session[:user_id] = user.id # login user
# nullify token, so it cannot be reused
internal_token.destroy
# reset User internal application password (maybe)
# user.update(password_token: SecureRandom.urlsafe_base64)
respond_to do |format|
format.json { render json: { success: true, url: welcome_url } }
format.html { redirect_to welcome_url }
end
else
redirect_to root_path
end
end
def destroy
session.delete(:user_id)
session[:user_id] = nil
#current_user = nil
redirect_to root_path
end
end
This Sessions Controller takes in the user_email and user_password_token to
make sure that these two match the same User internally before proceeding to
login. Then creates the user session with session[:user_id] and destroys the
internal_token, since it was a one time use only and is only used internally
within the application for this kind of authentication.
As well as, some kind of Welcome Controller for the Sessions create action to
redirect to after logging in
# app/controller/welcome_controller.rb
class WelcomeController < ApplicationController
def show
#user = current_user
redirect_to root_path unless current_user
end
private
def current_user
#current_user ||= User.find(session[:user_id])
end
end
Since this aplication uses
ActionCable, we
have already mounted the /cable path, now we need to setup a Channel that is
unique to a given User. However, since the User is not logged in yet, we use the
room_id value that was previously generated by the Tokens Controller show
action since its random and unique.
# app/channels/tokens_channel.rb
# Subscribe to `"tokens"` channel
class TokensChannel < ApplicationCable::Channel
def subscribed
stream_from "token_logins_#{params[:room_id]}" if params[:room_id]
end
end
That room_id was also embedded within the <head> (although it could a hidden
<div> element or the id attribtue of the QRCode, its up to you), which means
it can be pulled out to use in our JavaScript for receiving incoming boardcasts
to that room/QRCode; e.g.
// app/assets/javascripts/channels/tokens.js
var el = document.querySelectorAll('meta[name="room-id"]')[0];
var roomID = el.getAttribute('content');
App.tokens = App.cable.subscriptions.create(
{ channel: 'TokensChannel', room_id: roomID }, {
received: function(data) {
this.loginUser(data);
},
loginUser: function(data) {
var userEmail = data.user_email;
var userPasswordToken = data.user_password_token; // Mobile App's User token
var userData = {
user_email: userEmail,
user_password_token: userPasswordToken
};
// `csrf_meta_tags` value
var el = document.querySelectorAll('meta[name="csrf-token"]')[0];
var csrfToken = el.getAttribute('content');
var xmlhttp = new XMLHttpRequest();
// Handle POST response on `onreadystatechange` callback
xmlhttp.onreadystatechange = function() {
if (xmlhttp.readyState == XMLHttpRequest.DONE ) {
if (xmlhttp.status == 200) {
var response = JSON.parse(xmlhttp.response)
App.cable.subscriptions.remove({ channel: "TokensChannel",
room_id: roomID });
window.location.replace(response.url); // Redirect the current view
}
else if (xmlhttp.status == 400) {
alert('There was an error 400');
}
else {
alert('something else other than 200 was returned');
}
}
};
// Make User login POST request
xmlhttp.open(
"POST",
"<%= Rails.application.routes.url_helpers.url_for(
host: "localhost:3000", controller: "sessions", action: "create"
) %>",
true
);
// Add necessary headers (like `csrf_meta_tags`) before sending POST request
xmlhttp.setRequestHeader('X-CSRF-Token', csrfToken);
xmlhttp.setRequestHeader("Content-Type", "application/json");
xmlhttp.send(JSON.stringify(userData));
}
});
Really there is only two actions in this ActionCable subscription;
received required by ActionCable to handle incoming requests/events, and
loginUser our custom function
loginUser does the following:
Handles incoming data to build a new data object userData to POST back to
our application, which contains User information; user_email &
user_password_token; required to login over AJAX using an authentication
Token as the password (since its somewhat insecure, and passwords are usually
hashed; meaning that they unknown since they cannot be reversed)
Creates a new XMLHttpRequest() object to POST without jQuery, that sends a
POST request at the JSON login URL with the userData as login information,
whilst also appending the current HTML page CSRF token; e.g.
Otherwise the JSON request would fail without it
The xmlhttp.onreadystatechange callback function that is executed on a
response back from the xmlhttp.send(...) function call. It will unsubscribe
the User from the current room, since it is no longer needed, and redirect the
current page to the "Welcomw page" it received back in its response. Otherwise
it alerts the User something failed or went wrong
This will produce the following kind of application
You can access a copy of the project I worked on at the following URL:
Sonna/remote-url_qrcode-signin: ruby on rails - How can I implement Whatsapp life QR code authentication - Stack Overflow
The only this solution does not address is the rolling room token generation,
which would either require either a JavaScript library to generate/regenerate
the URL with the Room Token or a Controller Action that return a regenerated
QRCode as either image or HTML that can be immediately displayed within the
page. Either method still requires you to have some JavaScript that closes the
current connection and opens a new one with a new room/session token that can
used so that only it can receive mesages from, after a certain amount of time.
References:
Action Cable Overview — Ruby on Rails Guides
whomwah/rqrcode: A Ruby library that encodes QR Codes
Module: SecureRandom (Ruby 2_2_1)
#352 Securing an API - RailsCasts
Since I saw that the omniauth-dropbox gem, was made to:
Authenticate to the Dropbox REST API (v1).
I was happy i wouldn't need to develop all the redirections the OAuth implies.
But I can't find a way to make them work together :(
The omniauth-dropbox gem, works fine, alone, I get authenticated and stuff.
But what to save from the callback, so the dropbox-sdk would understand the user is authenticated?
How to do so the session.get_access_token would be automatically handled by omniauth-dropbox?
CODE
def dropbox
session = DropboxSession.new(MULTIAPI_CONFIG['dropbox']['appKey'], MULTIAPI_CONFIG['dropbox']['appSecret'])
session.get_request_token
authorize_url = session.get_authorize_url('myurl/auth/dropbox/callback')
print authorize_url
session.get_access_token
client = DropboxClient.new(session, ACCESS_TYPE)
object = client.metadata('/')
render :json => object
end
ERROR
Couldn't get access token. Server returned 401: Unauthorized.
It looks to me from https://github.com/intridea/omniauth/wiki like you can get env['omniauth.auth'] in your callback handler, and from there you can extract the credentials (token and secret). See https://github.com/intridea/omniauth/wiki/Auth-Hash-Schema.
Once you have the token and secret, you should be able to call session.set_access_token to tell the Dropbox SDK what credentials to use.
So I finally ended writing everything using the dropbox-sdk
Controller
class DropboxController < ApplicationController
def new
db_session = DropboxSession.new(MULTIAPI_CONFIG['dropbox']['appKey'], MULTIAPI_CONFIG['dropbox']['appSecret'])
begin
db_session.get_request_token
rescue DropboxError => e
render template: "multi_api/home/refresh"
end
session[:dp_request_db_session] = db_session.serialize
# OAuth Step 2: Send the user to the Dropbox website so they can authorize
# our app. After the user authorizes our app, Dropbox will redirect them
# to our 'dp_callback' endpoint.
auth_url = db_session.get_authorize_url url_for(:dp_callback)
redirect_to auth_url
end
def destroy
session.delete(:dp_authorized_db_session)
render :json => checkAuth
end
def isAuthenticated
render :json => checkAuth
end
def checkAuth
val = {'isAuthenticated' => false}
begin
unless not session[:dp_authorized_db_session]
dbsession = DropboxSession.deserialize(session[:dp_authorized_db_session])
client = DropboxClient.new(dbsession, MULTIAPI_CONFIG['dropbox']['accessType'])
val = {'isAuthenticated' => true}
end
rescue DropboxError => e
val = {'isAuthenticated' => false}
end
val
end
def callback
# Finish OAuth Step 2
ser = session[:dp_request_db_session]
unless ser
render template: "multi_api/home/refresh"
return
end
db_session = DropboxSession.deserialize(ser)
# OAuth Step 3: Get an access token from Dropbox.
begin
db_session.get_access_token
rescue DropboxError => e
render template: "multi_api/home/refresh"
return
end
session.delete(:dp_request_db_session)
session[:dp_authorized_db_session] = db_session.serialize
render template: "multi_api/home/refresh"
end
end
routes
get 'dp/logout', :to => 'dropbox#destroy'
get 'dp/login', :to => 'dropbox#new'
get 'dp/callback', :to => 'dropbox#callback', :as => :dp_callback
get 'dp/isAuthenticated', :to => 'dropbox#isAuthenticated'
multi_api/home/refresh.html.erb
<script type="text/javascript">
function refreshParent()
{
window.opener.location.reload();
if (window.opener.progressWindow)
window.opener.progressWindow.close();
window.close();
}
refreshParent();
</script>
Requesting dropbox
dbsession = DropboxSession.deserialize(session[:dp_authorized_db_session])
#client = DropboxClient.new(dbsession, MULTIAPI_CONFIG['dropbox']['accessType'])
I open a new tab when i want to authenticate a user to dropbox, when this is done I automatically refresh the original page and close the tab (see: multi_pi/home/refresh.html.erb).
Since I do all of this in javascript I need to know if the user has been authenticated successfully, that's why I provided a route dp/isAuthenticated which will return a json string containing a 'isAuthenticated' key set at true or false.
Connected users are not saved into a the database, only into the session. So when the session is destroyed they will have to re-authenticate. If you want them to be save into the database, then, you should dig into #smarx solution, using omniauth will be far easier.
I wrote my code here as an exemple for those who only want to depend on the ruby dropbox-sdk
omniauth-dropbox v0.2.0 uses the oauth1 API
dropbox-sdk v1.5.1 uses the oauth1 API
dropbox-sdk v1.6.1 uses the oauth2 API
With omniauth-dropbox v0.2.0 and dropbox-sdk v1.5.1, the following code in the controller action that omniauth redirects to works for me:
auth_hash = request.env['omniauth.auth']
access_token = auth_hash[:credentials][:token]
access_token_secret = auth_hash[:credentials][:secret]
session = DropboxSession.new(DROPBOX_APP_ID, DROPBOX_ADD_SECRET)
session.set_access_token(access_token, access_token_secret)
client = DropboxClient.new(session)
puts client.account_info.inspect
There might be a way to get omniauth-dropbox v0.2.0 and dropbox-sdk v1.6.1 working, but I haven't found it.
As James says, the most recent version of dropbox-sdk uses oauth2, so instead of omniauth-dropbox you need to use the oauth2 strategy:
https://github.com/bamorim/omniauth-dropbox-oauth2
Then the access_token will be present in the initial oauth response (as auth.credentials.token), and you can do with it as you will in the omniauth_callbacks_controller. I store it in an Authentication object.
For all those of you still looking for a tutorial for performing basically everything dropbox does, here's a tutorial I wrote a while ago:
http://blog.jobspire.net/dropbox-on-rails-4-0-ajax-using-dropbox-sdk/#more-54
You're welcome!
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