I made a test facebook app just to play around and I am using sessions to store the authentification. I am using omniauth. When I go to log in from http://fbbtest.heroku.com/ and then refresh the page the session is still saved and it says that I am logged in. When I try it from the canvas http://apps.facebook.com/herokutestapp/ it logs me in, redirects back and says that I am logged in but then when I manually refresh it then says that I am not logged in. Is there something special that I have to do with sessions in rails 3 so that it also works in the facebook canvas?
This what I currently have in my controllers and views
def index
end
def create
session['fb_auth'] = request.env['omniauth.auth']
session['fb_token'] = session['fb_auth']['credentials']['token']
session['fb_error'] = nil
redirect_to root_path
end
def destroy
clear_session
redirect_to root_path
end
def failure
clear_session
session['fb_error'] = 'In order to use this site you must allow us access to your Facebook data<br />'
redirect_to root_path
end
def clear_session
session['fb_auth'] = nil
session['fb_token'] = nil
session['fb_error'] = nil
end
Index View
<div class="container">
<h1>Heroku FB Test application</h1><br />
<div class="center"><br />
<%=session[:fb_error]%>
<% if session[:fb_token] %>
<p>
Successfully logged in.
</p>
<a href='logout'>Logout</a>
<% else %>
<%= session[:fb_error] %><br />
<%= link_to "Log in with Facebook", "/auth/facebook",:class => "popup", :"data-width" => 600, :"data-height" => 400 %> <br />
<p>
please log in
</p>
<% end %>
</div>
</div>
The problem you're possibly running into is that Rails CSRF forgery detection is creaming some part of your authentication because the requests are coming in as HTTP Method POST.
The first line in your ApplicationController is probably something like:
class ApplicationController < ActionController::Base
protect_from_forgery
[...]
Remove that 'protect_from_forgery' line and see if that helps with your problem. If that turns out to be the case, go back and set that up on a more limited basis (just the relevant controllers, see documentation here: http://api.rubyonrails.org/classes/ActionController/RequestForgeryProtection/ClassMethods.html )
There's an great example for getting Omniauth working at http://www.communityguides.eu/articles/16, and the full sample code is at https://github.com/markusproske/omniauth_pure. On that, they have the following:
class ServicesController < ApplicationController
before_filter :authenticate_user!, :except => [:create, :signin, :signup, :newaccount, :failure]
protect_from_forgery :except => :create
You need some variant of both those lines to make omniauth, facebook, and rails sessions play well together. If that doesn't work out for you, post your OmniAuth::Builder information from environment/production.rb (with details XXXed out) and any other related code in the controller you use for authentication, that will be helpful for debugging this.
It may be easier when developing rails apps using facebook to debug using http://tunnlr.com or another service (or just an ssh tunnel http://blog.kenweiner.com/2007/09/reverse-ssh-tunnel-for-facebook.html) that lets you run the debugger on your local machine, it is very helpful for figuring these sorts of problems out.
Sessions and cookies in Facebook iframes are very difficult to use, but not impossible. I've faced this a few times when trying to develop vote-once-per-day contests.
The solution is to use P3P headers. Honestly I'm not too sure how they work, but it reduces issues of cross-browser cookies in iframes - especially IE and Safari.
Add the following to the top of each page:
header('P3P:CP="IDC DSP COR ADM DEVi TAIi PSA PSD IVAi IVDi CONi HIS OUR IND CNT"');
This may not solve your issue exactly but hopefully it can help get you on the right path.
If I cookies.permanent.signed[:fb_auth] it allows me to return to the application in facebook without needing to re-log in. Is this the best way to get around sessions not working through iFrames?
This sounds like a third-party cookie issue. Are you certain that using cookies.permanent.signed works properly if you have only accessed the site through Facebook? Try clearing the cookies, restarting the browser, and then going to the Facebook canvas page and testing again.
In Firefox, try going to Tools->Options->Privacy and seeing if "Accept third-party cookies" is unchecked. If it is, try checking it and testing again.
It's not at all surprising that third-party cookies may be causing you problems, the confusing part is why using a persistent cookie should make any difference.
If you do verify that third-party cookies are the problem, I'm afraid there is no easy solution if you want the app to be accessible by everyone. You have to stop using cookies altogether, and maintain session state using only values passed by GET/POST.
Related
Explanation
I am wanting to halt the authorization process of a client app (running OAuth2) coming to the parent app (running Doorkeeper) in order to see which client app is requesting a login. That way I can then look up the clientID and dynamically build a custom login screen for the client app. Right now, my client goes to parent, AuthorizationController is called, but before new is called and I can get the params[:client_id], authenticate_resource_owner! is called with a before_action. That then sends the user to the login page if they are not already logged in with the parent. So, before I can get the param, it is being redirected.
Question
The authenticate_resource_owner! is held in a Doorkeeper helper file. I thought that I set it up correctly to bypass the default helper and go to mine where I can try and grab the param and save in sessions before the redirect, but I guess my route is not set up correctly and I can't find any documentation on how to correctly call it. Can anyone help?
Code
Code for setting up the client:
def setup_client
#client = Application.find_by(uid: params[:client_id])
session[:client_name] = #client.name
authenticate_resource_owner!
end
I know that the first 2 lines work as I placed them in the CustomAuthorizationsController with a byebug and it triggered after the login and before redirect back to client and showed the client name stored in a session variable.
In my config/routes.rb
use_doorkeeper do
controllers :applications => 'doorkeeper/custom_applications'
controllers :authorizations => 'doorkeeper/custom_authorizations'
helpers :doorkeeper => 'doorkeeper/doorkeeper'
end
Helper file is located in app/helpers/doorkeeper/doorkeeper_helper.rb
Error
When I start up my server I get:
: from ~/ruby-2.5.0/gems/doorkeeper-5.0.2/lib/doorkeeper/rails/routes/mapper.rb:12:in `instance_eval'
~/settingsParentApp/config/routes.rb:65:in `block (2 levels) in <top (required)>': undefined method `helpers' for #<Doorkeeper::Rails::Routes::Mapper:0x00007ffd539b9c10> (NoMethodError)
Conclusion
Am I even doing this right? Is there a simpler way built into Doorkeeper that I am not seeing to get this information to customize the login screen? Or is there some error that I am not seeing in how I am calling the helper file?
After thinking through my problem in order to ask this question, a solution dawned on me. I tested it out and it worked. I forgot that in a controller, the before_action statements are called in the order they are presented. So, my solution was just to reorder my statements to call the setup_client first before the authenticate_resource_owner!. This set up the session variable before redirecting to the login screen and then allowed me to have the variable available for use.
Code
Within my config/routes.rb file:
use_doorkeeper do
controllers :applications => 'doorkeeper/custom_applications'
controllers :authorizations => 'doorkeeper/custom_authorizations'
end
This custom route bypasses the doorkeeper default authorization controller and goes to a custom one which inherits from the default controller. So, all I need within this custom one is this code:
Found: app/controllers/doorkeeper/custom_authorizations_controller.rb
module Doorkeeper
class CustomAuthorizationsController < Doorkeeper::AuthorizationsController
before_action :setup_client
before_action :authenticate_resource_owner!
def setup_client
#client = Application.find_by(uid: params[:client_id])
session[:client_name] = #client.name
end
end
end
This code is then run before it looks to the Doorkeeper's default AuthorizationsController and thus calls setup_client first. The session variable is then saved and in the login screen I can call it this way:
<%
if session[:client_name].nil?
#client_name = ''
else
#client_name = ' for ' + session[:client_name]
end
#page_name = "Login" + #client_name
%>
And then in header of the page I call this within the HTML:
<h1><%= #page_name %></h1>
I may do more fancy things later, like saving client icons/logos and color schemes to make branding specific on the login page, but for now, this basic issue has been resolved. Thank you all for acting as my sounding board and problem-solving ducks... if you know of that reference. :-) Happy Coding!
I am building my first app using rails and i am struggling to give an account to each user. Its a simple app where a user creates a post and saves it. this post can be seen on index page. Until now we are ok. when i sign out, i register a new user and i log in. when i go to the index page, i can see what other user has saved. I want to have a private index page for each user where no one can see what users are saving in their app. I am using Gem 'Device" to authenticate my app, but i don't know how to make private their account? any ideas?
You have implemented authentication. Now you need to build authorization.
You have to define what data can be accessed by user.
Take a look at:
CanCanCan
With devise authentication technique, after logging in the user you can write a clean piece of code to render the posts of the logged in user. You can use session data to do it. the current_user helper method returns the model class relating to the signed in user. With devise, it ensures after authentication that, it has the helper methods ready to be used. So, the error with no such user with that id can be avoided.
#web/controller/post_controller.rb
before_filter :authenticate_user!
def list
#posts = current_user.posts
end
And then, use #posts in the list view to render the posts of the logged in user only.
<% #posts.each do |post| do%>
<%= post.content %>
<% end %>
I've just upgraded an app from Rails 3 to Rails 4, and I'm seeing a bunch of InvalidAuthenticityToken exceptions popping up. Digging in it looks like it is fairly common for our users to have multiple long-lived tabs open on our site. So I think what's happening is something like this: user Alice has three tabs open and her session expires. She logs back in on one of the tabs, which updates the authenticity token stored in her session. Then she returns to one of the other open tabs and tries to submit data, but she gets a 500 error from the InvalidAuthenticityToken error we raised.
It would clearly be nice to do some error handling for Alice so she doesn't get a 500 error. I'm wondering about best practices for this kind of situation. What would be a nice way to handle Alice's submission from the expired tab? I don't want to expire the current session, because that would be super annoying from the user's perspective ("I just logged in, you dolt!"). Ideally, I just want the user to reload the page, which would result in the correct authenticity token being present in the form. Or should I be doing something different so that the long-lived tabs that are open notice that the session has expired and force a reload? This would probably be sub-optimal from the user's point of view, because they liked having that page ready and easily accessible to keep referencing, which is why they left it open in the tab in the first place.
In my rails 4.1.1 app I had the same problem. I solved it by adding this code to ApplicationController. I found this solution here.
rescue_from ActionController::InvalidAuthenticityToken, with: :redirect_to_referer_or_path
def redirect_to_referer_or_path
flash[:notice] = "Please try again."
redirect_to request.referer
end
This way any controller that inherits from ApplicationController will handle the error with a redirect to the page the form was submitted from with a flash message to give the user some indication of what went wrong. Note this uses the hash syntax introduced in Ruby 1.9. For older versions of Ruby you will need to use :with => :redirect_to_referer_or_path
The solution to this problem can be divided into 2 phases. Phase 1 addresses the issue of ActionController::InvalidAuthenticityToken error and phase 2 deals with the issue of long tabs waiting idly.
Phase 1(1st variation)
One way to go about is redirect the user back to their location before the error. For ex. if Alice has 3 tabs open, the first one expires and Alice logs in again in it because she was browsing on it. But when she moves to tab 3 which has URL 'http://example.com/ex' and submits a form. Now instead of displaying her an error we can redirect her back to 'http://example.com/ex' with her submitted form values already pre-filled in the form for easy use.
This can be achieved by following this approach:
1) ApplicationController - Add this function:
def handle_unverified_request
flash[:error] = 'Kindly retry.' # show this error in your layout
referrer_url = URI.parse(request.referrer) rescue URI.parse(some_default_url)
# need to have a default in case referrer is not given
# append the query string to the referrer url
referrer_url.query = Rack::Utils.parse_nested_query('').
merge(params[params.keys[2]]). # this may be different for you
to_query
# redirect to the referrer url with the modified query string
redirect_to referrer_url.to_s
end
2) You need to include a default value for all your form fields. It will be the name of that field.
...
<% f.text_field, name: 'email', placeholder: 'Email', value: params[:email] %>
...
This way whenever Alice will submit a form with wrong authenticity_token she will be redirected back to her form with the original values she submitted and she will be shown a flash message that kindly retry your request.
Phase 1(2nd variation)
Another way to go about is just redirect Alice back to the form which she submitted without any pre-filled values.
This approach can be achieved by:
1) ApplicationController - Add this function:
def handle_unverified_request
flash[:error] = 'Kindly retry.'
redirect_to :back
end
Phase 2
To tackle the problem of long awaited tabs you can take the help of SSEs. Rails 4 has ActionController::Live for handling SSEs.
1) Add this to any controller:
include ActionController::Live
...
def sse
response.headers['Content-Type'] = 'text/event-stream'
sse = SSE.new(response.stream, retry: 2000, event: 'refresh') # change the time interval to your suiting
if user_signed_in? # check if user is signed-in or not
sse.write('none')
else
sse.write('refresh')
end
ensure
sse.close
end
2) Give the above function a GET route in your routes file. Lets call this route '/sse'
3) Add this in your layout:
<% if user_signed_in? %> # check if user is signed-in or not
<script>
var evtSource = new EventSource("/sse");
evtSource.addEventListener('refresh', function(e){
if(e.data == 'refresh'){
window.location = window.location.href;
}
});
</script>
<% end %>
Note: using EventSource is not supported by all browsers. Please check out the Browser compatibility section.
Source:
rails 4 redirect back with new params & MDN: Using server-sent events
Answer by Durrell is perfectly alright, I am just providing an alternative way to write the same thing. This needs to go in ApplicationController.
rescue_from ActionController::InvalidAuthenticityToken do |_exception|
flash[:alert] = 'Please try again.'
redirect_back fallback_location: root_path
end
I've just added and configured the devise gem. It's working great except for blocking my form's autosave AJAX calls.
At the top of my controller, I have:
before_filter :authenticate_user!
My AJAX call comes to the same controller:
def autosave
#TODO: update relative entry
#TODO: verify user logged in
#TODO: verify entry belongs to relative user
render content_type: 'text/xml', inline: "<result status='ok' />"
end
Of course I could instead declare my before filter like so:
before_filter :authenticate_user!, except: :autosave
..but this offers nothing to prevent anyone from calling this controller function at any time.
What would be the best way to allow this function to be called? I still want to ensure that only logged in users can make the call and that the given record they're editing belongs to them.
Not entirely sure what you're trying to do, but it sounds like you just need to add a bit of logic to your autosave. With this setup the function will still be called per se, but whether or not it does anything is a another mater entirely unless the conditions are right.
def autosave
#Verify that the user is signed in and he has ownership of entry
if !current_user.nil? && entry.user == current_user
render content_type: 'text/xml', inline: "<result status='ok' />"
else
#do nothing nothing or redirect with error here
end
end
Since you're making an AJAX post you have to provide some additional information to your ajax call as a security countermeasure, namely a CSRF security token. See Devise not setting current_user on Ajax post even though correct x-csrf-token is included in request header for an example AJAX request using a security token. Also, make sure you also include <%= csrf_meta_tag %> in your head tag.
Im having a really strange problem with Devise. I have a route set up that accepts both get and post requests. On a get, it shows the form, on the post, it submits it.
When I send a post XHR to the route, when it gets there it tells me that I am not logged in, and sends me a 401 unauthorized. After that I have to log in, and then I can try again.
I have been trying to figure this out for hours, all I have been able to figure out is that my controller method is not getting called. I put in my own custom auth before filter, and it just confirmed that by the time my rails app gets called, the user is no longer logged in.
Also, if I open up the form, but dont submit it, I can continue on as normal. Somewhere in that XHR it is making devise log me out.
If you have any ideas please help, I have no idea what is going on right now...
Thanks
-Scott
EDIT: Adding relevant pieces of code
routes.rb
match 'projects/:p/filebox' => 'projects#show', :via => ["get","post"], :as => 'project_filebox'
projects_controller.rb
before_filter :authenticate_user! # <--- By the time this gets called, the user is logged out
def show
# ^^^^ Doesnt get called. Logger shows that it recognized route though
logger.debug "-----------projects#show"
logger.debug "Current user logged in:"+user_signed_in?.to_s
form that is being submitted
<form class="upload" action="<%= project_filebox_path(#project) %>?n=7&cType=<%= cType %>&fid=<%= fid %>" method="post" enctype="multipart/form-data">
<input type="file" name="file" multiple/>
<button>Upload</button>
<div>Add / Drag Files To Upload</div>
</form>
Javascript that is uploading the XHR
formDataUpload = function (files, xhr, settings) {
var formData = new FormData(),
i;
$.each(getFormData(settings), function (index, field) {
formData.append(field.name, field.value);
});
for (i = 0; i < files.length; i += 1) {
formData.append(settings.fieldName, files[i]);
}
xhr.send(formData);
}
If I missed some relevant piece of code let me know
There's not that much to go on here other than the JS, but there's a really strong change you're having the problem because the CSRF token isn't being set as part of your request. This has changed in various Rails 3.0.x releases so hard to know for sure without code.
One dead simple test would be to turn off CSRF (e.g. remove protect_from_forgery from ApplicationController). If it works, you have the answer and need to make sure the token gets passed around or you otherwise handle forgery protection.
I ran into the same problem when I have two forms in one page, one of them post to an route within then application, the other post to an outside address. John Paul Ashenfelter is right that it has nothing to do with JS.
Like you, I do not want to disable CSRF for the entire site. I ended up disable protect from forgery for the method that is posting to the off site address in the controller:
protect_from_forgery :except => [:some_method]
And in the form that is creating this CSRF problem:
<%= form_for :some_model, authenticity_token: false do%>