Rails + Oauth + Tumblr - api

I came across an interesting issue with Tumblr's oauth implementation that I wanted to document for others. When ever i used the code below i received a "400 Bad Request", when I inspected the respose in wireshark I discovered this was coming back from tumblr "Out-of-band ("oob") callbacks are not supported by this implementation.". This is wwierd because my tumblr application has a call back field that I had explicitly set.
# Your tumblr details:
key = "Your Key"
secret = "Your Secret"
site = "http://www.tumblr.com"
# puts 'Setting up request'
#consumer = OAuth::Consumer.new(key, secret, { :site => site,
:request_token_path => '/oauth/request_token',
:authorize_path => '/oauth/authorize',
:access_token_path => '/oauth/access_token',
:http_method => :post
})
puts 'Asking for token, dies here.'
#request_token = #consumer.get_request_token()
puts 'Got Token Storing'
session[:request_token]=#request_token
puts 'Redirecting'
redirect_to #request_token.authorize_url

Turns out that call back field in tumblr's api isn't being taken into account.
you need to change this line:
#request_token = #consumer.get_request_token()
to be:
#request_token = #consumer.get_request_token(:oauth_callback => "http://192.168.2.115:5000/oauth/callback")
That seems to make it all work.

Related

Rails/Devise/SAML Metadata Incorrect (not working with PingFederate)

Let me preface this question by saying that I'm new to SAML and barely understand how it works.
The Setup
I'm using the devise_saml_authenticatable gem with a Rails 4 app to achieve SSO. The Rails app acts as the service provider (SP). To test my setup, I created a OneLogin developer account and set up a SAML Test Connector (IdP w/attr w/ sign response) using the following attributes:
Configuration Tab
Audience: mysubdomain.onelogin.com
Recipient: http://mysubdomain.myapp.local:3000/saml/auth
ACS (Consumer) URL Validator: ^http://mysubdomain.myapp.local:3000/saml/auth$
ACS (Consumer) URL: http://mysubdomain.myapp.local:3000/saml/auth
Single Logout URL: http://mysubdomain.myapp.local:3000/saml/idp_sign_out
SSO Tab
Issuer URL: https://app.onelogin.com/saml/metadata/589819
SAML 2.0 Endpoint (HTTP): https://mysubdomain.onelogin.com/trust/saml2/http-post/sso/589819
SLO Endpoint (HTTP): https://mysubdomain.onelogin.com/trust/saml2/http-redirect/slo/589819
SAML Signature Algorithm: SHA-1
SHA Fingerprint: 60:9D:18:56:B9:80:D4:25:63:C1:CC:57:6D:B9:06:7C:78:BB:2C:F1
X.509 Certificate:
-----BEGIN CERTIFICATE-----
MIIEFzCCAv+gAwIBAgIUQYRVa1MQpUh0gJaznmXSF/SPqnowDQYJKoZIhvcNAQEF
BQAwWDELMAkGA1UEBhMCVVMxETAPBgNVBAoMCEZpcm1QbGF5MRUwEwYDVQQLDAxP
bmVMb2dpbiBJZFAxHzAdBgNVBAMMFk9uZUxvZ2luIEFjY291bnQgOTI1MzEwHhcN
MTYwOTIxMTU0NzQwWhcNMjEwOTIyMTU0NzQwWjBYMQswCQYDVQQGEwJVUzERMA8G
A1UECgwIRmlybVBsYXkxFTATBgNVBAsMDE9uZUxvZ2luIElkUDEfMB0GA1UEAwwW
T25lTG9naW4gQWNjb3VudCA5MjUzMTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC
AQoCggEBALGVgocBj0ciHM3uKlWIcofPhOtzfJw1XpAdNynAvPtbCl7WE5+sLBoQ
ZF+oZ7Dl+wRW6DHMJCl9DdKcOaQA6/gr5bwt78IzZ8hWMoKQEPih+E0km6rKLYA8
M52vxtJxGs8Iqx60QvPEePQFMOA+xg73OExfM7W5LnXwNz/Pxgsr3lBif5oCC76j
SaTCFroV+TSjfOaYMW/lZrsS79KRIzA9I5XwUBe3bC8bsfQmZXgddCrkQUNSGGaS
7/jtFUlQ94+lAL+l3yoAiNAE6+mt48qqmyLfkKibXvnZ8dwuO272wpY4fEM+vFRy
pYrTajqvhY3hYIq8dLw3ominE5VECl8CAwEAAaOB2DCB1TAMBgNVHRMBAf8EAjAA
MB0GA1UdDgQWBBSxiuvTPxwOhh2pupID+tuyKCeceTCBlQYDVR0jBIGNMIGKgBSx
iuvTPxwOhh2pupID+tuyKCeceaFcpFowWDELMAkGA1UEBhMCVVMxETAPBgNVBAoM
CEZpcm1QbGF5MRUwEwYDVQQLDAxPbmVMb2dpbiBJZFAxHzAdBgNVBAMMFk9uZUxv
Z2luIEFjY291bnQgOTI1MzGCFEGEVWtTEKVIdICWs55l0hf0j6p6MA4GA1UdDwEB
/wQEAwIHgDANBgkqhkiG9w0BAQUFAAOCAQEAYBe+5d3zpLZ7fcf3l3rXYeIxcpN+
9D2YZCbxsrBhY2Am4YE9nN+RaJXeDqeRBNtpayCZVxfHnXexRo1n7wxwTmosiydi
9yE7SY2xZf+3feQreF25atnn4tzVhxYONaX1njZMIt/TNa7A9aeDfHSD+vwSuYYB
hGxKT6HOkEAEBiXCZ/FcVNiB0D8bRwQhiJ3BTzXDfqHrmq8QYdn3Ejlqo62vMl6W
XeMXUoyv6cUc64Ap6E+XtEQI1E8YB5R8GtTs3Y1Oa2dD6yWyCyVJ20+Hi7IWAqXC
EfqstqXB7FoQ2rAt39cepnu1SOarvEYDMwYIaVNF3hoyodBybJJsAwAnCQ==
-----END CERTIFICATE-----
In my devise.rb I have the following configuration:
config.saml_create_user = false
config.saml_update_user = true
config.saml_default_user_key = :email
config.saml_session_index_key = :session_index
config.saml_use_subject = true
config.idp_settings_adapter = IdPSettingsAdapter
config.idp_entity_id_reader = DeviseSamlAuthenticatable::DefaultIdpEntityIdReader
Here is my IdPSettingsAdapter:
class IdPSettingsAdapter
def self.settings(idp_entity_id)
company = Company.find_by(idp_entity_id: idp_entity_id)
if company.present?
{
assertion_consumer_service_url: company.assertion_consumer_service_url,
assertion_consumer_service_binding: company.assertion_consumer_service_binding,
name_identifier_format: company.name_identifier_format,
issuer: company.issuer,
idp_entity_id: company.idp_entity_id,
authn_context: company.authn_context,
idp_slo_target_url: company.idp_slo_target_url,
idp_sso_target_url: company.idp_sso_target_url,
idp_cert_fingerprint: company.idp_cert_fingerprint
}
else
{}
end
end
end
Note that my user model Contact belongs_to Company, and that the SSO settings are stored in the Company model.
Here are my saml routes:
devise_for :contacts, skip: :saml_authenticatable, controllers: {
registrations: "registrations",
sessions: "sessions",
passwords: "passwords",
confirmations: "confirmations"
}
devise_scope :contact do
get '/sign_in' => 'sessions#new'
get '/sign_out' => 'sessions#destroy'
# SSO Routes
get 'saml/sign_in' => 'saml_sessions#new', as: :new_user_sso_session
post 'saml/auth' => 'saml_sessions#create', as: :user_sso_session
get 'saml/sign_out' => 'saml_sessions#destroy', as: :destroy_user_sso_session
get 'saml/metadata' => 'saml_sessions#metadata', as: :metadata_user_sso_session
match 'saml/idp_sign_out' => 'saml_sessions#idp_sign_out', via: [:get, :post]
end
Lastly here is my SamlSessionsController:
require "ruby-saml"
class SamlSessionsController < SessionsController
include DeviseSamlAuthenticatable::SamlConfig
skip_before_filter :verify_authenticity_token, raise: false
before_action :authorize_viewer, except: [:metadata]
protect_from_forgery with: :null_session, except: :create
def new
idp_entity_id = Company.friendly.find(#_request.env['HTTP_HOST'].split('.')[0]).idp_entity_id
request = OneLogin::RubySaml::Authrequest.new
action = request.create(saml_config(idp_entity_id))
redirect_to action
end
def metadata
idp_entity_id = Company.friendly.find(#_request.env['HTTP_HOST'].split('.')[0]).idp_entity_id
meta = OneLogin::RubySaml::Metadata.new
render :xml => meta.generate(saml_config(idp_entity_id)), content_type: 'application/samlmetadata+xml'
end
def create
#idp_entity_id = Company.friendly.find(#_request.env['HTTP_HOST'].split('.')[0]).idp_entity_id
response = OneLogin::RubySaml::Response.new(params[:SAMLResponse], settings: saml_config(#idp_entity_id))
if !response.is_valid?
puts "SAML FAILED WITH ERROR: "
puts response.errors
end
super
end
def idp_sign_out
company = Company.friendly.find(request.subdomain.downcase)
idp_entity_id = Company.friendly.find(#_request.env['HTTP_HOST'].split('.')[0]).idp_entity_id
if params[:SAMLRequest] && Devise.saml_session_index_key
saml_config = saml_config(idp_entity_id)
logout_request = OneLogin::RubySaml::SloLogoutrequest.new(params[:SAMLRequest], settings: saml_config(idp_entity_id))
resource_class.reset_session_key_for(logout_request.name_id)
# binding.pry
sign_out current_contact if contact_signed_in?
redirect_to company.after_slo_url.present? ? company.after_slo_url : 'https://' + company.issuer
# redirect_to generate_idp_logout_response(saml_config(idp_entity_id), logout_request.id)
elsif params[:SAMLResponse]
#Currently Devise handles the session invalidation when the request is made.
#To support a true SP initiated logout response, the request ID would have to be tracked and session invalidated
#based on that.
if Devise.saml_sign_out_success_url
redirect_to Devise.saml_sign_out_success_url
else
redirect_to action: :new
end
else
head :invalid_request
end
end
protected
# Override devise to send user to IdP logout for SLO
def after_sign_out_path_for(_)
request = OneLogin::RubySaml::Logoutrequest.new
request.create(saml_config)
end
def generate_idp_logout_response(saml_config, logout_request_id)
OneLogin::RubySaml::SloLogoutresponse.new.create(saml_config, logout_request_id, nil)
end
end
The Problem
When I manually save map the settings from my OneLogin adapter to my Company model (see screenshot), I'm able to authenticate as a user of my app using OneLogin as the identity provider (IdP). However now I need to provide a client with the XML metadata representing the app's setup. When I go to /saml/metadata.xml, I get the following configuration, which according to my client, is incorrect. The client didn't offer any further details about what the problem is. They are using PingFederate, if that matters.
<?xml version='1.0' encoding='UTF-8'?>
<md:EntityDescriptor ID='_a3581975-b73d-4784-a106-bafd61e15f87' xmlns:md='urn:oasis:names:tc:SAML:2.0:metadata'>
<md:SPSSODescriptor AuthnRequestsSigned='false' WantAssertionsSigned='false' protocolSupportEnumeration='urn:oasis:names:tc:SAML:2.0:protocol'>
<md:NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:transient</md:NameIDFormat>
<md:AssertionConsumerService Binding='urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST' Location='https://mysubdomain.myapp.local:3000/saml/auth' index='0' isDefault='true'/>
</md:SPSSODescriptor>
</md:EntityDescriptor>
My question is, what am I doing wrong here and how can I correct it? As I said, I barely understand how SAML works under the hood.
There is no EntityID defined on that metadata XML.
If you try to verify the XML on a validation tool you will get
Line: 2 | Column: 0 --> Element
'{urn:oasis:names:tc:SAML:2.0:metadata}EntityDescriptor': The
attribute 'entityID' is required but missing.
If you review ruby-saml code, the EntityID is added to the metadata XML if a settings.issuer is defined. Can you verify if that data is provided? Maybe company.issuer that I see at IdPSettingsAdapter class has an empty value.

How do I pass in the 'hd' option for OpenID Connect (Oauth2 Login) using the Google Ruby API Client?

The "Using Oauth 2.0 for Login" doc lists the 'hosted domain' parameter as a valid authentication parameter, but using the Google API Client for Ruby linked at the bottom I don't see how to pass it along with my request. Anyone have an example?
OK, wasn't perfect, but I just passed it to the authorization_uri attribute on the authorization object like so
client = Google::APIClient.new
client.authorization.authorization_uri(:hd => 'my_domain')
I still had trouble updating the Addressable::URI object to save the change (kept getting a "comparison of Array with Array failed" error), but this was good enough for me to use.
I couldn't get it to work using the Google::APIClient but managed to get it working using the OAuth2::Client like this
SCOPES = [
'https://www.googleapis.com/auth/userinfo.email'
].join(' ')
client ||= OAuth2::Client.new(G_API_CLIENT, G_API_SECRET, {
:site => 'https://accounts.google.com',
:authorize_url => "/o/oauth2/auth",
:token_url => "/o/oauth2/token"
})
...
redirect client.auth_code.authorize_url(:redirect_uri => redirect_uri,:scope => SCOPES,:hd => 'yourdomain.com')

Deprecated offline_access on facebook with RoR

We have a problem in our RoR app. We are using a facebook authentication with omniauth, and searching the user friends with Koala. But lately, when we try to show a friend photo, we got this error:
Koala::Facebook::APIError in Homes#show
Showing /home/daniel/Homes/app/views/shared/_event.html.erb where line #19 raised:
OAuthException: Error validating access token: Session has expired at unix time 1328727600. The current unix time is 1328802133.
Extracted source (around line #19):
16: <img src="../assets/friends-icon.png" alt="User profile apicture" height="33" width="43">
17: <% if current_user %>
18: <% event.friends_in_event(#person).each do |f| %>
19: <%= link_to(image_tag(f.fb_picture, :size => "43x33"), person_path(f.id)) %>
20: <% end %>
21: <% end %>
22: </div>
The authentication works good, but facebook has already deprecated the offline_access option, that was working good, but now, we have this issue.
is It any way to extends the access_token?, or are there another solution?.
This is our omniauth.rb
Rails.application.config.middleware.use OmniAuth::Builder do
provider :facebook, ENV['FB_KEY'], ENV['FB_SECRET'],
{ :scope => 'email,offline_access,user_photos,publish_stream',
:client_options => { :ssl => { :ca_path => "/etc/ssl/certs" } } }
end
And our koala.rb
Koala.http_service.http_options = {
:ssl => { :ca_path => "/etc/ssl/certs" }
}
Thanks in advance.
There are 2 solutions to this problem:
Extend the user's access token:
As per this article on the Facebook docs, you may request a 60-day extension on a user's access token. However, if the user does not return within that period, this method won't help you.
You can find a PHP code snippet to do this at this StackOverflow question.
To do this, send a post to this API endpoint: https://graph.facebook.com/oauth/access_token?client_id=APP_ID&client_secret=APP_SECRET&grant_type=fb_exchange_token&fb_exchange_token=EXISTING_ACCESS_TOKEN
Catch the OAuthException and request a new access token:
Facebook provides a PHP code snippet outlining this solution on their dev blog.
Basically, you follow these steps:
Make a call to the graph with the user's current access_token.
If the call succeeds, the access_token is fine. If it throws an OAuthException, redirect the user to https://www.facebook.com/dialog/oauth?client_id=APP_ID&redirect_uri=CALLBACK_URL
The user will be sent to that URL and then redirected to your CALLBACK_URL with a code in the parameters.
Send a post to the following URL with the code to obtain a new access_token: https://graph.facebook.com/oauth/access_token?client_id=APP_ID&redirect_uri=CALLBACK_URL&client_secret=APP_SECRET&code=CODE&display=popup
Read the post on their dev blog for more information.
Edit (adding example Ruby on Rails code):
Add the following to the top of your ApplicationController:
rescue_from Koala::Facebook::APIError, :with => :handle_fb_exception
Add the following protected method to your ApplicationController:
def handle_fb_exception exception
if exception.fb_error_type.eql? 'OAuthException'
logger.debug "[OAuthException] Either the user's access token has expired, they've logged out of Facebook, deauthorized the app, or changed their password"
oauth = Koala::Facebook::OAuth.new
# If there is a code in the url, attempt to request a new access token with it
if params.has_key? 'code'
code = params['code']
logger.debug "We have the following code in the url: #{code}"
logger.debug "Attempting to fetch a new access token..."
token_hash = oauth.get_access_token_info code
logger.debug "Obtained the following hash for the new access token:"
logger.debug token_hash.to_yaml
redirect_to root_path
else # Since there is no code in the url, redirect the user to the Facebook auth page for the app
oauth_url = oauth.url_for_oauth_code :permissions => 'email'
logger.debug "No code was present; redirecting to the following url to obtain one: #{oauth_url}"
redirect_to oauth_url
end
else
logger.debug "Since the error type is not an 'OAuthException', this is likely a bug in the Koala gem; reraising the exception..."
raise exception
end
end
The Koala calls were all taken from the following 2 tutorials:
https://github.com/arsduo/koala/wiki/OAuth
https://github.com/arsduo/koala/wiki/Koala-on-Rails
For those of you who don't have time to make this change, I found that you can disable this migration in Settings -> Advanced. The name of the option is "Remove offline_access permission:"

Posting to Facebook OG with HTTParty only works from the rails console?

I'm having a really weird problem with my rails app and facebook's open graph beta. Whenever I post an action to an object, Facebook returns an error that seems to indicate the URL of the object can't be reached, or the og scraper isn't scraping the URL correctly.
However, when I take the URL the app is generating for the post to Facebook and manually use the HTTParty gem to post it, it works.
Here's my code:
class Post < ActiveRecord::Base
FB_CONFIG = YAML.load_file("#{Rails.root}/config/initializers/facebook.yml")[Rails.env]
def self.to_facebook_og(obj, obj_id, verb, auth, extra)
#requires that a user has granted `publish_actions`
found_obj = obj.classify.constantize.find(obj_id) #find the actual object we're talking about
post_url = self.construct_facebook_action_url(obj, found_obj, verb, auth, extra) #create the URL
begin
ret = HTTParty.post(post_url)
logger.info "Facebook Post Action Response = #{ret}"
rescue HTTParty::ResponseError => e #handle any errors
logger.error {"FACEBOOK Response #{ret.code} / #{e.inspect}"}
flash.alert {"There was a Facebook problem. Please try again."}
return
end
end
def self.construct_facebook_action_url(obj, found_obj, verb, auth, extra)
base = 'https://graph.facebook.com/'
uid = auth.uid
namespace = FB_CONFIG['namespace']
token = "?access_token=#{auth.token}"
og_url = "#{obj}=http://theshortestfiction.com/#{obj.pluralize}/#{found_obj.id}"
fb_url = base + uid + '/' + namespace + ':' + verb + token + '&' + og_url + extra
logger.info fb_url
fb_url
end
def self.lint_og_object(obj_url)
lint_ret = HTTParty.post("https://developers.facebook.com/tools/lint/?url=#{obj_url}&format=json")
logger.info "Facebook Linter Response = #{lint_ret}"
end
end
When an object is read via its controller's show method, the app calls Post.to_facebook. From my logs, I can see that that Post.construct_facebook_action_url is constructing the proper url (because like I said, I can pull the URL from the logs and manually post it from the console). So, I'm assuming there's so problem with how I'm passing the URL to HTTParty? Facebook seems able to tell what object URL it should be looking at. Why does the code I've written not work, but manually in the console, it does?
Even weirder -- once there's been a sucessful post action on the object once, the code seems to work consistently. Facebook insists the problem is that the objects' URLs aren't reachable, but I can't understand how they're not, since I can browse to them.
I think this is actually a timeout issue.
I had the exact same issue as you, using HTTParty and getting the URL can't be reached error.
I moved the code to a background process using Resque and it fixed the problem.

Error with facebook access token

My problem is on facebook callback url. I am using fbgraph gem on Rails 3.0.
I ask for extended permissions on my tab application. So in the callback I wait code parameter and access_token.
I extract this code from fbgraph official GIT repository.
def authorize
begin
#auth.client.authorization_code = params[:code]
#In access_token line should return me access__token but throw a error message (see below)
access_token = #auth.client.access_token! # => Rack::OAuth2::AccessToken
#facebook_user = FbGraph::User.me(access_token).fetch # => FbGraph::User
#MORE CODE WITHOUT IMPORTANCE
redirect_to :controller => "dashboard", :action => "index"
rescue Exception => e
logger.info(e.message)
end
end
Throw this error message:
Rack::OAuth::Client::Error # => #status = 400, Message => Missing redirect uri
Please I need help quickly. Excuse me and thanks in advance
I'm using the fb_graph gem which is similar. In order to get the access_token you also need to supply the callback URI - this is the fb_graph version:
client.redirect_uri = "http://your.callback.uri"
client.authorization_code = params[:code]
access_token = client.access_token!
Update:
Looking at the fbgraph gem documentation I think you need to replace these two lines:
#auth.client.authorization_code = params[:code]
access_token = #auth.client.access_token!
With this:
access_token = #auth.client.authorization.process_callback(params[:code], :redirect_uri => callback_url)
To be honest I looked at using the fbgraph gem but the documentation was so bad that I switched to fb_graph instead which is similar and actually has some useful examples in the documentation.