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:"
Related
I'm having troubles restoring password with devise_token_auth. and Angular2-Token. I'm successfully receiving the email with the link to update my password. But I'm getting an 401 Unauthorized response when submiting the new password.
Front end. I'm getting the token from the URL with urlParams.get('token')
onPasswordUpdate() {
let token = this.urlParams.get('token');
var obj = Object.assign(this._updatePasswordData, { reset_password_token: token })
this._tokenService.patch('auth/password/', obj ).subscribe(
res => res,
error => error
);
}
Back end response.
Started PATCH "/api/auth/password/" for 127.0.0.1 at 2016-12-01 21:17:48 +0100
Processing by DeviseTokenAuth::PasswordsController#update as JSON
Parameters: {"reset_password_token"=>"[FILTERED]", "password"=>"[FILTERED]", "password_confirmation"=>"[FILTERED]"}
Completed 401 Unauthorized in 1ms (Views: 0.4ms | ActiveRecord: 0.0ms)
In the link of the email I get the following token : reset_password_token=HneZDoKTMCLF3_SLfnxy
When I visit the link, the user record gets updated with the following attributes :
reset_password_token: "aa3cba76c7b1d8f78cde6856f43e1cce57f5fc8e5301842733de677eff909bc1"
tokens: {}
Then in the browser URL I get the following token=agejaip2SqOp9nvwE1GAHQ&uid
And then the user record get updated with the following attribues :
...
reset_password_token: "HneZDoKTMCLF3_SLfnxy",
tokens: {"pv9i1BDTM29ezep0KSPzpA"=>{"token"=>"$2a$10$cS9gbe9UBICcgphZHRAENOMS6NlEe0Em1cNufY3LSRTPE.hRMabvi", "expiry"=>1481834221}}
...
It seems to me that the token I get back in URL is not correct.
Those anyone have an idea ?
Sorry It's a bit hard to explain.
Many thanks.
rails (4.2.4)
devise_token_auth (0.1.34)
devise (= 3.5.1)
angular2-token: 0.2.0-beta.1
I faced similar challenges recently, and this was how I solved it.
Expose the 'access-token', 'expiry', 'token-type', 'uid', 'client' for your backend. Check here and here
config.middleware.use Rack::Cors do
allow do
origins '*'
resource '*',
:headers => :any,
:expose => ['access-token', 'expiry', 'token-type', 'uid', 'client'],
:methods => => [:get, :post, :options, :delete, :put, :patch]
end
end
Set your redirect_url of path: /password, method: POST. Check info here
We need to modify the reset_password_instructions.html.erb to point it to the api GET /auth/password/edit. More information provided here.
E.g. if your API is under the api namespaces:
<%= link_to 'Change my password', edit_api_user_password_url(reset_password_token: #token, config: message['client-config'].to_s, redirect_url: message['redirect-url'].to_s) %>
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.
Why does the Rack middleware fail to redirect when coupled with default GET and POST login routes and 401 handling in the Sinatra app?
Relevant Shield middleware extract :
module Shield
class Middleware
attr :url
def initialize(app, url = "/login")
#app = app
#url = url
end
def call(env)
tuple = #app.call(env)
if tuple[0] == 401
[302, headers(env["SCRIPT_NAME"] + env["PATH_INFO"]), []]
else
tuple
end
end
private
def headers(path)
{ "Location" => "%s?return=%s" % [url, encode(path)],
"Content-Type" => "text/html",
"Content-Length" => "0"
}
end
def encode(str)
URI.encode_www_form_component(str)
end
end
end
View full source code (104 lines/2.8kb).
Here a relevant extract of the Sinatra app:
# application_controller.rb
class ApplicationController < Sinatra::Base
helpers Shield::Helpers
use Shield::Middleware, "/login"
...
get '/noway' do
error(401) unless authenticated(User)
erb :app_noway
end
get '/login' do
erb :login
end
post "/login" do
if login(User, params[:login], params[:password])
remember(authenticated(User)) if params[:remember_me]
redirect(params[:return] || "/")
else
redirect "/login"
end
end
end
Full source code (basic app displaying the problem behavior), for easy and immediate perusal: https://github.com/shieldtest/shieldtest
The repository is ready for a "clone and rackup" with database, env and all. Login credentials; email: shield#example.org, password: shield.
Problem When accessing a protected route (/noway), the middleware injects a authentication process, as intended. But after the successful autentication, the subsequent redirect always defaults to root, instead of the return URL for the protected page (/noway).
Solution needed The protected page (/noway) should be redirected to automatically after authenticating successfully via Shield.
Visual walk-through
Step 1 (below): At the Sinatra main page. Click link to protected page (/noway)
Step 2 (below): Redirected to /login correctly, as no user is authenticated. Enter correct login credentials correctly.
PROBLEM BEHAVIOR - redirected to main instead of the protected pageStep 3A (below): After entering correct login credentials: sent back to main page (again)
TESTING LOGIN - protected page is accessible now (manually, by clicking page again)Step 4 (below): At the main page. Click the protected page (/noway) again => Access granted
The params[:return] was never forwarded to the POST request, it seems.
So, a 'dirty fix' would be to grab the return params and pass it via the login form to the POST request. This yields the desired behavior:
#login.rb
...
<% if params[:return] %>
<input type='hidden' name='redirect' value="<%= params[:return] %>">
<% end %>
...
And then redirecting to the redirect params from the login form:
#application_controller.rb
post "/login" do
if login(User, params[:login], params[:password])
...
redirect to params[:redirect] || "/"
...
end
end
Still, I would have preferred to understand why the middleware didn't perform as expected and how to fix/store this return params via the Rack middleware.
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.
This is something that has been driving me mad over the past few days. I have an action which allows authenticated users to upload assets to the site. I know that the controller action is correct as I can run through the process manually however I want to test it using rspec.
I have to use the request helper so I can reuse an authenticated session which is a :given for this set of tests.
it "should allow authenticated file uploads" do
file = File.open(a_valid_file)
mock_file = mock("file")
mock_file.stub!(:path).and_return(file.path)
request( resource(:assets), :method => "POST",
:params => { :file =>
{:tempfile => mock_file, :filename => File.basename(file.path)} }
)
end
If I breakpoint inside the spec it all works nicely, however when I run the spec and try to access the path in the controller action through the debugger I get this:
e file[:tempfile].path
NoMethodError Exception: undefined method `path' for "#[Spec::Mocks::Mock:0x3fda2a4736c0 #name=\"file\"]":String
My guess is that the stub!(:path) is not being set for whatever mock object is making it through the request.
The question is: Am I going about the right way for testing file uploads and if not what is another way?
I was doing it wrong. By using request it was calling to_s on all paramaters, so my mock object was being passed as "#[Spec::Mocks::Mock:0x3fda2a4736c0 #name=\"file\"]". That will teach me to pay more attention to exception output.
Instead I should use multipart_post and stub out the authentication calls in a block.
it "should allow authenticated file uploads" do
file = File.open(a_valid_file)
multipart_post( resource(:assets), :method => "POST",
:params => { :file => file } ) do |controller|
controller.stub!(:ensure_authenticated).and_return(true)
controller.session.stub!(:user).and_return(User.first)
)
end