I'm trying to write an integration with the LinkedIn API for my Rails app using OAuth2 but I'm having a hard time figuring out how to make authenticated API calls. I can't seem to find any resources on the topic that don't rely on the linkedin gem. I already have that gem installed in my app, but need to make some API calls that are not possible using that gem.
What I want to do is set up a controller with some endpoints that my Angular client can ping. When the client pings the controller, it should in turn ping the LinkedIn API. Once it gets a response it will perform some operations to store the information in the database and then pass it along to the Angular client.
I found a single code sample in Ruby from LinkedIn, which I'm attempting to modify into a controller for my app. Here is my controller:
class Employers::LinkedinApiController < ApplicationController
require 'net/http'
layout 'employers/layouts/application'
before_action :authorize_viewer
skip_after_filter :intercom_rails_auto_include
REDIRECT_URI = 'http://app.mysite.local:3000/linkedin/callback'
STATE = SecureRandom.hex(15)
CLIENT = parameterize(client.auth_code.get_token(STATE, redirect_uri: REDIRECT_URI))
def get_update(update_key=nil)
company = current_employer.company
update_key = company.social_media_posts.where("linkedin IS true AND linkedin_id IS NOT NULL").last.linkedin_id
page_id = company.linkedin.page_id
end_point = "https://api.linkedin.com/v1/companies/#{page_id}/updates/key=#{update_key}?format=json"
access_token = OAuth2::AccessToken.new(client, CLIENT['token'], {
:mode => :header,
:header_format => 'Bearer %s'
})
response = access_token.get(end_point)
# Do something with the response to save it to the database
# Pass response on to Angular client
respond_to do
format.json { render json: response }
end
end
private
#Instantiate your OAuth2 client object
def client
OAuth2::Client.new(
ENV['LINKEDIN_APP_ID'],
ENV['LINKEDIN_APP_SECRET'],
authorize_url: "/uas/oauth2/authorization?response_type=code",
token_url: "/uas/oauth2/accessToken",
site: "https://www.linkedin.com"
)
end
def parameterize(string)
parameters = {}
string.split('?')[-1].split('&').each do |param|
parameters[param.split('=')[0]] = param.split('=')[-1]
end
parameters
end
def authorize_viewer
if !employer_signed_in?
redirect_to new_employer_session_path
end
end
end
In the code sample from LinkedIn, the only way to get data from the accept method is to ping the authorize method, whose callback points to the accept method. What I can't figure out is how I can ping the get_update method in my controller and make an authenticated API call, without having to set up an authorize method that calls back to get_update.
How can I do this?
Related
I'm trying to implement OAuth as a sign-in option on my app. Most providers have been working really well, but Twitter has been giving me a ton of trouble.
I'm able to get a request token and be sent an oauth_verifier. However, after that I'm unable to get an authorized response from Twitter using the oauth-ruby gem.
I cannot use Devise or Omniauth in my setup.
Here is a minimalist and annotated version of my code:
# controllers/authorizations_controller.rb
# route: POST /auth/twitter
def create
if params[:oauth_token].blank? # This gets hit as the first part of the process. This entire block works as expected
oauth = Oauth::Twitter.get_token
session['oauth_token'] = oauth
render json: { oauth_token: oauth.token } # The user authorizes and is redirected (to this same action)
else # Once authorized, the oauth_token params is set and this gets executed
Oauth::Twitter.new(params, session['oauth_token'])
end
end
# models/oauth/twitter.rb
class Oauth::Twitter < Oauth::Base
def self.get_token # This seems to work
consumer.get_request_token(oauth_callback: 'http://localhost:3000') # This must be set to 'http://localhost:3000' per my frontend library
end
def initialize(params, request_token_hash)
consumer = self.class.consumer
request_token = OAuth::RequestToken.from_hash(consumer, request_token_hash) # This returns the expected RequestToken object
request_token.get_access_token(oauth_verifier: params[:oauth_verifier]) # This is where the 401 is thrown. params['oauth_verifier'] is confirmed to be set.
end
private
def self.consumer
OAuth::Consumer.new(
Rails.application.secrets['TWITTER_KEY'],
Rails.application.secrets['TWITTER_SECRET'],
:site => "https://api.twitter.com"
)
end
end
Unfortunately, I don't get any more information than a thrown `401 Authorization Required). Between the Twitter docs for sign-in via OAuth (more specifically, the oauth/access_token endpoint) and a similar example using the same OAuth gem, I can't see where my mistake lies.
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
Using the 'oauth2' gem and a Heroku server, I have managed to create a client object and redirect the user to the login site:
client = OAuth2::Client.new(
"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
:authorize_url => "/oauth/authorize",
:token_url => "/oauth/token",
:site => "https://connect.xxxxxxxxxx.com")
redirect_to(client.auth_code.authorize_url(:redirect_uri => 'https://xxxxx.herokuapp.com/callback'))
The browser afterwards redirects itself to the callback link as intended, something like:
https://xxxxx.herokuapp.com/callback?code=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
I need to access the authorization code to then send a post request for the access token and refresh token, but being totally new to Ruby and Ruby on Rails, I am not sure how to get the callback and parse the code out. All of the dozen tutorials/documentation I've researched just mention that the authorization code should be 'magically obtained,' but I'm not sure how exactly that works explicitly. I tried creating a 'callback' controller and view to no avail - is there something missing in the routes files possibly? Help is much appreciated!
Your CallbackController will start to look like this maybe:
class CallbackController < ApplicationController
def index
access_token = client.auth_code.get_token(params[:code], redirect_uri: 'https://xxxxx.herokuapp.com/callback')
# Now you have an OAuth2::AccessToken object that you can either use to:
# - make direct requests to the API
# - or access access_token.token, access_token.refresh_token, access_token.expires_at, access_token.expires_in and store those
# somewhere for later use
# http://www.rubydoc.info/github/intridea/oauth2/OAuth2/AccessToken
end
private
def client
#client ||= OAuth2::Client.new(
"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
authorize_url: "/oauth/authorize",
token_url: "/oauth/token",
site: "https://connect.xxxxxxxxxx.com"
)
end
end
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
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!