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
Related
I have create several applications that communicate with our central auth server via doorkeeper. I want to make some applications accessible/inaccessible for specific users.
Is there a way to restrict access to specific oauth_applications and return a 401?
I believe the easiest way to achieve this would be the following:
In your doorkeeper application, change the Users table to include a permissions relationship. Something like, User -> has many -> permissions
And those permissions could contain just the name of the application you want to give them access to, (Or the ID of the application, you choose)
Then, in your config/initializer/doorkeeper.rb - inside Doorkeeper::JWT.configure - you add which applications that particular user can access inside the token payload, something like:
token_payload do |opts|
...
token[:permissions] = user.permissions.pluck(:application_name)
end
If you are using Doorkeeper without JWT, you can still pass extra information to the token by prepending a custom response to the ResponseToken object like so:
Doorkeeper::OAuth::TokenResponse.send :prepend, CustomTokenResponse
and CustomTokenResponse just need to implement the methods body, like so:
module CustomTokenResponse
def body
additional_data = {
'username' => env[:clearance].current_user.username,
'userid' => #token.resource_owner_id # you have an access to the #token object
# any other data
}
# call original `#body` method and merge its result with the additional data hash
super.merge(additional_data)
end
end
extra information can be found in Doorkeepers' wiki: https://github.com/doorkeeper-gem/doorkeeper/wiki/Customizing-Token-Response
and in the Doorkeeper JWT gem: https://github.com/doorkeeper-gem/doorkeeper-jwt#usage
On 9 Feb 2020 a new configuration option was introduced in Doorkeeper to exactly do this.
Therefore, you can configure config/initializer/doorkeeper.rb:
authorize_resource_owner_for_client do |client, resource_owner|
resource_owner.admin? || client.owners_whitelist.include?(resource_owner)
end
I wanted the same behaviour. I use the resource_owner_authenticator block in config/initializer/doorkeeper.rb. When a user has one or more groups which are connected with an Oauth application it can continue.
rails g model UserGroup user:references group:references
rails g model GroupApplications group:references oauth_application:references
resource_owner_authenticator do
app = OauthApplication.find_by(uid: request.query_parameters['client_id'])
user_id = session["warden.user.user.key"][0][0] rescue nil
user = User.find_by_id(user_id)
if !app && user
user
elsif app && user
if !(user.groups & app.groups).empty?
user
else
redirect_to main_app.root_url, notice: "You are not authorized to access this application."
end
else
begin
session['user_return_to'] = request.url
redirect_to(new_user_session_url)
end
end
end
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.
I have a rails app and I want to use the twitter api with the account of a user (to make favs, tweets, etc ...).
Here is my problem, I want the user to be able to create several "Project" with one different twitter account by project.
That is why I want to get the access_token and the access_token_secret but store them in my Project model and not in my User model.
I don't understand how I can do that. All the stuffs I found explain every time the way to do it with Omniauth and a "sign up"/"Sign in" of the User.
I'm very new to this Oauth / callback / API stuff so I'm a bit lost.
Is there an easy way to just get the access_token and the access_token_secret and store it where I want ?
I managed to do it with saving information i needed in the session.
def ask_twitter_authorisation
session[:info] = #project
redirect_to "/auth/twitter"
end
and then use it in my callback
def twitter_callback
session_hash = session[:info]
#project = Project.find(session_hash["id"])
#project.t_access_token = auth_hash.credentials.token
#project.t_access_token_secret = auth_hash.credentials.secret
#project.save
end
I'm in the process of setting up Doorkeeper and OAuth2 for one of my Rails Applications. My goal is to allow api access to a user depending on their access_token, so that only a user can see their 'user_show' json. So far I have my development and production applications set up and authorized on the 'oauth2/applications' route.
My '/config/initializers/doorkeeper.rb'
Doorkeeper.configure do
# Change the ORM that doorkeeper will use.
# Currently supported options are :active_record, :mongoid2, :mongoid3,
# :mongoid4, :mongo_mapper
orm :active_record
# This block will be called to check whether the resource owner is authenticated or not.
resource_owner_authenticator do
# Put your resource owner authentication logic here.
# Example implementation:
User.find_by_id(session[:current_user_id]) || redirect_to('/')
end
end
and my '/api/v1/user/controller.rb' looks as such:
class Api::V1::UserController < Api::ApiController
include ActionController::MimeResponds
before_action :doorkeeper_authorize!
def index
user = User.find(doorkeeper_token.resource_owner_id)
respond_with User.all
end
def show
user = User.find(doorkeeper_token.resource_owner_id)
respond_with user
end
end
I have tried to gain access to the OAuth Applications table to see what is being created but I cannot access it in the rails console.
Thanks in advance for the insight!
It seems that Doorkeeper doesn't find any token.
Make sure you're sending it, either from url with ?access_token=#{token} or ?bearer_token=#{token}, either giving this token in headers using Bearer Authorization.
You also need to have in mind that a token could be associated only to an app, without a resource owner. So resource_owner_id value could be nil even with a valid token. It depends on what grant flow you're using (client credential flow is not associated with a resource owner). See https://github.com/doorkeeper-gem/doorkeeper/wiki#flows
For the OAuth tables, try with Doorkeeper::AccessToken.all in a rails console.
Hope this helped
Does anyone know of a maintained gem that handles user authentication for the Zendesk API through an existing Rails 3 application?
I asked Zendesk IT and got sent to https://github.com/tobias/zendesk_remote_auth, but it does not look rails 3 compatible and has not been updated since 2009.
I think the article in our docs gives the impression that Zendesk SSO is difficult when in fact it is pretty easy (http://www.zendesk.com/api/remote-authentication).
# reference http://www.zendesk.com/api/remote-authentication
# you need to be a Zendesk account admin to enable remote auth (if you have not already)
# go to Settings > Security, click "Enabled" next to Single Sign-On
# three important things to pay attention to:
# Remote Login URL, Remote Logout URL, and shared secret token
# for testing on a Rails 3 application running on localhost, fill in the Remote Login URL to map
# to http://localhost:3000/zendesk/login (we will need to make sure routes for that exist)
# fill in Remote Logout URL to http://localhost:3000/zendesk/logout
# copy the secret token, you'll need it later
# first, let's create those routes in config/routes.rb
namespace :zendesk do
match "/login" => "zendesk#login" # will match /zendesk/login
match "/logout" => "zendesk#logout" # will match /zendesk/logout
end
# Above I've mapped those requests to a controller named "zendesk" but it can be named anything
# next we want to add our secret token to the application, I added this in an initializer
# config/initializers/zendesk_auth.rb
ZENDESK_REMOTE_AUTH_TOKEN = "< your token >"
ZENDESK_REMOTE_AUTH_URL = "http://yourcompany.zendesk.com/access/remote/"
# Assuming we have a controller called zendesk, in zendesk_controller.rb
require "digest/md5"
class ZendeskController < ApplicationController
def index
#zendesk_remote_auth_url = ZENDESK_REMOTE_AUTH_URL
end
def login
timestamp = params[:timestamp] || Time.now.utc.to_i
# hard coded for example purposes
# really you would want to do something like current_user.name and current_user.email
# and you'd probably want this in a helper to hide all this implementation from the controller
string = "First Last" + "first.last#gmail.com" + ZENDESK_REMOTE_AUTH_TOKEN + timestamp.to_s
hash = Digest::MD5.hexdigest(string)
#zendesk_remote_auth_url = "http://yourcompany.zendesk.com/access/remote/?name=First%20Last&email=first.last#gmail.com×tamp=#{timestamp}&hash=#{hash}"
redirect_to #zendesk_remote_auth_url
end
def logout
flash[:notice] = params[:message]
end
end
# Note that the above index action defines an instance variable #zendesk_remote_auth_url
# in my example I simple put a link on the corresponding view that hits ZENDESK_REMOTE_AUTH_URL, doing so
# will cause Zendesk to hit your applications Remote Login URL (you defined in your Zendesk SSO settings) and pass a timestamp back in the URL parameters
# BUT, it is entirely possible to avoid this extra step if you just want to go to /zendesk/login in your app
# notice I am either using a params[:timestamp] if one exists or creating a new timestamp with Time.now
This example is quite simplistic but I just want to illustrate the basic mechanics of Zendesk SSO. Note that I'm not touching the more complicated issue of creating new users or editing existing ones, just logging in users who have an existing Zendesk account.
There is an updated example code from zendesk
# Using JWT from Ruby is straight forward. The below example expects you to have `jwt`
# in your Gemfile, you can read more about that gem at https://github.com/progrium/ruby-jwt.
# Assuming that you've set your shared secret and Zendesk subdomain in the environment, you
# can use Zendesk SSO from your controller like this example.
class ZendeskSessionController < ApplicationController
# Configuration
ZENDESK_SHARED_SECRET = ENV["ZENDESK_SHARED_SECRET"]
ZENDESK_SUBDOMAIN = ENV["ZENDESK_SUBDOMAIN"]
def create
if user = User.authenticate(params[:login], params[:password])
# If the submitted credentials pass, then log user into Zendesk
sign_into_zendesk(user)
else
render :new, :notice => "Invalid credentials"
end
end
private
def sign_into_zendesk(user)
# This is the meat of the business, set up the parameters you wish
# to forward to Zendesk. All parameters are documented in this page.
iat = Time.now.to_i
jti = "#{iat}/#{rand(36**64).to_s(36)}"
payload = JWT.encode({
:iat => iat, # Seconds since epoch, determine when this token is stale
:jti => jti, # Unique token id, helps prevent replay attacks
:name => user.name,
:email => user.email,
}, ZENDESK_SHARED_SECRET)
redirect_to zendesk_sso_url(payload)
end
def zendesk_sso_url(payload)
"https://#{ZENDESK_SUBDOMAIN}.zendesk.com/access/jwt?jwt=#{payload}"
end
end