Rails 4 authenticity token - both in header and form hidden input? - ruby-on-rails

I'm attempting to get full page caching in Rails but I've hit a big of a snag with regards to CSRF - or perhaps just my understanding of it. I currently have the form_authenticity_token string stored in a cookie that JS can access and rewrite the header tags with.
There are two places I find tokens in my generated HTML:
1) In the head
<meta name="csrf-token" content="[hash]">
2) Inside a form's hidden input element
<input type="hidden" name="authenticity_token" value="[different hash]">
As indicated, these hashes are different from one another (in development mode where caching isn't enabled). Why are they different? Why is it that I can delete the head meta tags and leave the form input alone and the request is allowed? Yet when I delete the form input tag and leave the headers the request is rejected?
Effectively this means the head tags are useless, no? I can rewrite the form input tag to the value in my cookie just like I did with the header tags, but since they are different from one another I'm cautious as to what the end result might mean especially when it comes to full page caching.
Application Controller contains:
protect_from_forgery with: :exception
before_filter :csrf_cookie
def csrf_cookie
cookies['authenticity-token'.freeze] = {
value: form_authenticity_token,
expires: 1.day.from_now,
secure: (Rails.env.staging? || Rails.env.production?)
}
end

Browsing SO on another issue led me to the answer. In short, Rails helps out jQuery users by inserting the CSRF token into ajax requests automatically. It looks for it in the meta tags.
So having the CSRF token inside the form is useful for when submitting POST requests and having it in the head is useful for saving time/effort/mistakes with ajax requests.
Perhaps it's good to have it in both also because you may want to do an ajax request when there isn't a form present. If there IS a form and javascript is disabled, having it in the header doesn't do anyone any favours as it won't be included in the POST request.
As to why they are different, I can only guess it has something to do with the algorithm at the time of generation...but that's neither here nor there as both tokens work.

Related

Rails - form doesn't submit any data during AJAX call - file upload - 422 status code

I have encountered a problem when trying to upload files with bootsy gem, but the problem is not related to bootsy itself. Bootsy generates a form with a following definition:
<form class="bootsy-upload-form form-inline" id="new_image" data-type="json" enctype="multipart/form-data" action="/bootsy/image_galleries/67/images" accept-charset="UTF-8" data-remote="true" method="post">
<input name="utf8" value="✓" type="hidden">
<input name="authenticity_token" id="authenticity_token" value="1pQMR5j33OKupMg4TSnQafLQx1BxzWjkP1KqTfZafqDRfGqwB8r2J4FzRE+dyuoHVOw/W0qd1FZ1JoJsJFThDQ==" type="hidden">
...
When I try to upload a file, this line is executed:
this.uploadInput.closest('form').submit();
I have added alert before it so I would see how the serialized data of the form looks like and all the fields are shown as expected (including authenticity token etc.):
alert(this.uploadInput.closest('form').serialize());
When the form is submitted, no data is send during POST request, only headers, nothing is seen in browser inspector, nothing can be seen in log files of rails, it just looks like this:
Started POST "/bootsy/image_galleries/47/images" for ::1 at 2016-02-05 11:20:04 +0100
Processing by Bootsy::ImagesController#create as HTML
Parameters: {"image_gallery_id"=>"47"}
Can't verify CSRF token authenticity
Completed 422 Unprocessable Entity in 0ms (ActiveRecord: 0.0ms)
FATAL -- :
ActionController::InvalidAuthenticityToken - ActionController::InvalidAuthenticityToken:
_ actionpack (4.2.4) lib/action_controller/metal/request_forgery_protection.rb:181:in `handle_unverified_request'_
I have authenticity token generated in the form, I have the token in meta tag, everything looks fine, no error is thrown anywhere. I also tried to create sample app which is similar to my real project and it worked as expected, form normally submitted data - when I tried to compare HTML code and javascript events attached to both, they were almost similar expect a few parts because of other gems like ajax_pagination etc., but there were no parts, which should cause such a behavior.
I am using Rails 4.2.4, turbolinks are disabled using the attribute data-no-turbolink on body element, project uses bootstrap and contains JS libraries like jQuery, underscore, parsley, momentjs.
I would appreciate any thoughts, what could went wrong, why the form should not submit any data, where could be a problem. Thanks in advance for any tip.
UPDATE:
Just to clarify things, I have taken a picture of a state before sending using AJAX - this javascript is part of jquery_ujs = RoR adapter for jQuery. You can see, that data contains all the form fields before send:
But data is NOT being send to the server:
On the other hand in my second working project, data is being sent:
UPDATE 2:
Just a few more information, bootsy gem, which is responsible for creation of the form uses remotipart to attach files to the request. Still...I was debugging the javascript and was unable to identify the problem. Both projects have the same version of jquery and remotipart, also the same version of rails. Looks like this will stay a mystery.
UPDATE 3:
So I have almost resolved the issue - uploading is now working, it looks like it was a problem with order of javascript libraries. I'll post the result as soon as I pinpoint the exact issue - I'll reverse the changes and try to fix it again.
The Issue was with an order of javascript libraries included in application.js. Bootsy was defined after jquery-fileupload. It seems, that Bootsy must be defined before it, with this setup, both works well. So state before:
//= require jquery-fileupload
//= require bootsy
//= require bootsy/locales/cs.js
after (working):
//= require bootsy
//= require bootsy/locales/cs.js
//= require jquery-fileupload
It seems that the Authenticity Token is not being sent to the server as expected.
I would look at the Network requests tab in the browser and inspect the body of the post.
I think setting processData to false is correct. According to the JQuery docs,
By default, data passed in to the data option as an object (technically, anything other than a string) will be processed and transformed into a query string
Also, you can add this in the controller before the check for the token:
before_filter { puts params }
I would not skip the Authenticity Token before filter.
Also, in your image "But data is NOT being send to the server:", you are looking at the headers. The Authenticity Token is sent in the body.
I think the issue is the way the form is being encoded. You should not encode it. Set the Content Type to multipart/form-data. The Content Type is currently set to 'application/x-www-form-urlencoded'.
The server is URL decoding the form, which is combining all the fields into one. That's why it doesn't find the Authenticity Token.
According to Sending multipart/formdata with jQuery.ajax, you should set contentType to false in the ajax request. Alternately, you can set it to multipart/form-data.
By default the form only sends normal data.If you need to send image along with the post request you can use remotipart gem. It works great .I have used it myself.
I have two theories, none of them has nothing to do with your form:
First theory:
With the update of your question, if you look closely, cookies doesn't contain _csrf_token. If _csrf_token is not passed then Rails will raise that error message.
I can't see if your form is generating their own hidden field with this value. It may be that one of your projects do so and the other does't. You should check it out.
If that was the problem, there are various solutions that you can find. One of it is to include [csfr_meta_tag] in HTML head of the view. Check this out for more details.
Second theory:
If you are performing a CORS request, your browser makes an additional OPTIONS request first just to check that you do have permission to access this resource.
Apparently, _session_id cannot be sent in CORS, unless you enable it. To do so, you need to handle the OPTIONS request. This is done by modifying Access-Control-Allow-Origin, Access-Control-Allow-Methods and Access-Control-Allow-Headers headers on controller before_filter and after_filter methods.
Here lives a gist that shows an example of this (Forget the skip_before_filter line!). If you want to get more details of appointed headers values, check this resource out.
These are my theories, I hope that help you!
UPDATE
If both of your projects are using Rails v4.2, maybe the one that works has set protect_from_forgery like this:
protect_from_forgery
# OR maybe
protect_from_forgery with: :null_session
And the one that doesn't work has set protect_from_forgery like this:
protect_from_forgery with: :exception
The difference between them is the application behavior when a request is found to be unverified. See more here.
Without seeing your actual code, we are left at guesses and shooting in the dark, however as obvious as this may seem, I assume you've followed all gem instructions? The install generator uses asset pipeline and sets up initializers etc. This seems rather obvious but did you make sure to do whitelist bootsy param on the model in the app that's not working? (as per https://github.com/volmer/bootsy )
private
def post_params
params.require(:post).permit(:title, :content, :bootsy_image_gallery_id)
end
the CRSF authentication issues are the cross site scripting issues.To overcome this issue, do either,
skip_before_action :verify_authenticity_token in your controller
or you can comment this line in application controller.
protect_from_forgery with: :exception
Visit this link for more description on this

How can we circumvent these remote forms drawback?

In an effort to have everything translateable in our website ( including the error messages for the validations ), we switched almost all of our forms to remote forms. While this helps with the ability to translate error messages, we have encountered other problems, like:
if the user clicks on the submit button multiple times, the action gets called multiple times. If we have a remote form for creating a new record in the database, and assuming that the user's data is valid, each click will add a new object ( with the exact same contents ). Is there any way of making sure that such things cannot happen?
Is there somewhere I could read about remote forms best practices? How could I handle the multiple clicks problem? Is switching all the forms to remote forms a very big mistake?
There is a rails 3 option called :disable_with. Put this on input elements to disable and re-label them while a remote form is being submitted. It adds a data-disable-with tag to those inputs and rails.js can select and bind this functionality.
submit_tag "Complete sale", :disable_with => "Please wait..."
More info can be found here
Easy, and you can achieve that in many ways depending your preferences:
Post the form manually simply using an ajax request and while you wait for the response disable/hide (or whatever you need) the form to ensure the user can't keep doing posts as crazy. Once you get the response from the server, again you can allow the user to post again (cleaning the form first), or show something else or redirect it to another page or again whatever you need.
Use link_to :remote=>true to submit the form and add a callback function to handle the response and also to disable/hide (or whatever you need) the form when it's submitted
Add a js listener to the form to detect when it's submitted and then disable/hide/whatever the form
As you see, there are lots of different ways to achieve what you need.
EDIT: If you need info about binding or handling a form submit from js here you'll find very easy and interesting examples that may help you to do what I suggested you! jQuery Submit
I have remote forms extensively myself, and in most cases I would avoid them. But sometimes your layout or UX demands for on-the-fly drop-down forms, without reloading or refreshing the complete page.
So, let me tackle this in steps.
1. Preventing Normal form double-post
Even with a normal form, a user could double-click your button, or click multiple times, if the user does not get a clear indication that the click has been registered and the action has started.
There are a lot of ways (e.g. javascript) to make this visible, but the easiest in rails is this:
= f.button :submit, :disable_with => "Please wait..."
This will disable the button after the first click, clearly indicating the click has been registered and the action has started.
2. Handling the remote form
For a remote form it is not that much different, but the difference most likely is: what happens afterward ?
With a remote form you have a few options:
In case of error: you update the form with the errors.
you leave the form open, allowing users to keep on entering the data (I think this is your case?)
you redirect the users to some place.
Let me handle those cases. Please understand that those three cases are completely standard when doing a normal form. But not when doing a remote call.
2.1 In case of error
For a remote form to update correctly, you have to do a bit more magic. Not a lot, but a bit.
When using haml, you would have a view called edit.js.haml which would look something like
:plain
$('#your-form-id').replaceWith('#{j render(:partial => '_form') }');
What this does: replace the complete haml with only the form. You will have to structure your views accordingly, to make this work. That is not hard, but just necessary.
2.2 Clearing the form
You have two options:
* re-render the form completely, as with the errors. Only make sure you render the form from a new element, not the just posted one!!
* just send the following javascript instead:
$('#your-form-id').reset();
This will blank the form, and normally, that would effectively render any following clicking useless (some client validation could block posting until some fields are filled in).
2.3 Redirecting
Since you are using a remote form, you can't just redirect. This has to happen client-side, so that is a tad more complicated.
Using haml again this would be something like
:plain
document.location.href = '#{#redirect_uri}';
Conclusion
To prevent double (triple, quadruple, more) posts using remote forms you will have to
disable the button after first click (use :disable_with)
clear the form after succesful submission (reset the form or render with a new element)
Hope this helps.
The simplest solution would be to generate a token for each form. Then your create action could make sure it hasn't been used yet and determine whether the record should be created.
Here's how I would go about writing this feature. Note that I haven't actually tested this, but the concept should work.
1.
Inside the new action create a hash to identify the form request.
def new
#product = Product.new
#form_token = session["form_token"] = SecureRandom.hex(15)
end
2.
Add a hidden field to the form that stores the form token. This will be captured in the create action to make sure the form hasn't been submitted before.
<%= hidden_field_tag :form_token, #form_token %>
3.
In the create action you can make sure the form token matches between the session and params variables. This will give you a chance to see if this is the first or second submission.
def create
# delete the form token if it matches
if session[:form_token] == params[:form_token]
session[:form_token] = nil
else
# if it doesn't match then check if a record was created recently
product = Product.where('created_at > ?', 3.minutes.ago).where(title: params[:product][:title]).last
# if the product exists then show it
# or just return because it is a remote form
redirect_to product and return if product.present?
end
# normal create action here ...
end
Update: What I have described above has a name, it is called a Synchronizer (or Déjà vu) Token. As described in this article, is a proper method to prevent a double submit.
This strategy addresses the problem of duplicate form submissions. A synchronizer token is set in a user's session and included with each form returned to the client. When that form is submitted, the synchronizer token in the form is compared to the synchronizer token in the session. The tokens should match the first time the form is submitted. If the tokens do not match, then the form submission may be disallowed and an error returned to the user. Token mismatch may occur when the user submits a form, then clicks the Back button in the browser and attempts to resubmit the same form.
On the other hand, if the two token values match, then we are confident that the flow of control is exactly as expected. At this point, the token value in the session is modified to a new value and the form submission is accepted.
I hate to say it, but it sounds like you've come up with a cure that's worse than the disease.
Why not use i18n for translations? That certainly would be the 'Rails way'...
If you must continue down this route, you are going to have to start using Javascript. Remote forms are usually for small 'AJAXy things' like votes or comments. Creating whole objects without leaving the page is useful for when people might want to create lots of them in a row (the exact problem you're trying to solve).
As soon as you start using AJAX, you have to deal with the fact that you'll have to get into doing some JS. It's client-side stuff and therefore not Rail's speciality.
If you feel that you've gone so far down this road that you can't turn back, I would suggest that the AJAX response should at least reset the form. This would then stop people creating the same thing more than once by mistake.
From a UI/UX point of view, it should also bring up a flash message letting users know that they successfully created the object.
So in summary - if you can afford the time, git reset and start using i18n, if you can't, make the ajax callback reset the form and set a flash message.
Edit: it just occurred to me that you could even get the AJAX to redirect the page for you (but you'd have to handle the flash messages yourself). However, using a remote form that then redirects via javascript is FUGLY...
I've had similar issues with using a popup on mouseover, and not wanting to queue several requests. To get more control, you might find it easier to use javascript/coffeescript directly instead of UJS (as I did).
The way I resolved it was assigning the Ajax call to a variable and checking if the variable was assigned. In my situation, I'd abort the ajax call, but you would probably want to return from the function and set the variable to null once the ajax call is completed successfully.
This coffeescript example is from my popup which uses a "GET", but in theory it should be the same for a "POST" or "PUT".
e.g.
jQuery ->
ajaxCall = null
$("#popupContent").html " "
$("#popup").live "mouseover", ->
if ajaxCall
return
ajaxCall = $.ajax(
type: "GET"
url: "/whatever_url"
beforeSend: ->
$("#popupContent").prepend "<p class=\"loading-text\">Loading..please wait...</p>"
success: (data) ->
$("#popupContent").empty().append(data)
complete: ->
$"(.loading-text").remove()
ajaxCall = null
)
I've left out my mouseout, and timer handling for brevity.
You can try something like that for ajax requests.
Set block variable true for ajax requests
before_filter :xhr_blocker
def xhr_blocker
if request.xhr?
if session[:xhr_blocker]
respond_to do |format|
format.json, status: :unprocessable_entity
end
else
session[:xhr_blocker] = true
end
end
end
Clear xhr_blocker variable with an after filter method
after_filter :clear_xhr_blocker
def clear_xhr_blocker
session[:xhr_blocker] = nil
end
I would bind to ajax:complete, (or ajax:success and ajax:error) to redirect or update the DOM to remove/change the form as necessary when the request is complete.

Unexpected behaviour with AntiForgeryToken and ValidateAntiForgeryToken

I've started using AntiForgeryToken in some of my forms to prevent cross site request forgery. However I am getting some weird behaviour and just wanted to clarify whether this is a bug or just me doing something wrong. I am using the Html.AntiForgeryToken() call in my form. I then use the [ValidateAntiForgeryToken] attribute in the action method that the form posts to. I'm not using a salt at this point.
My understanding is that Html.AntiForgeryToken() generates a hidden input with a name of __RequestVerificationToken and a cookie named __RequestVerificationToken_Lw__, which should both contain the same value.
The behaviour I am experiencing however is that:
The cookie always has the same value no matter how many times you
GET the page
The hidden input has a different value every time you GET the page
The ValidateAntiForgeryToken validates every time, even from a
different site in a CSRF scenario.
If I change the value of the hidden input in the foreign site, the
token doesn't validate (expected behaviour, but why does it validate
when the hidden input/cookie value is different?)
Anyone got any ideas?
For number 3, are you including the hidden field in your CSRF scenario?
The safety of the AntiForgeryToken is that the hidden input exists only in the page served by your domain, and cannot be copied or captured by another domain. If you have mocked up a test which passes the hidden input, then that is not a valid test.
I suggest you read this article from Phil Haack: Anatomy of a Cross-site Request Forgery Attack

Is it possible to RedirectToRoute over POST instead of GET?

I'm trying to perform a redirect from one controller to another while passing along some parameters. Unfortunately the value of the parameters are long and obnoxious looking in the GET query string. Is there anyway I can use POST instead on the redirect to get a nicer looking URL?
Thanks.
As everyone said, you cannot redirect to post.
However, you can avoid ugly URLs by sticking your values in TempData instead of the route.
You can't do a proper POST redirect, but you can use JavaScript in the browser to mimic a POST redirect:
<form id="myform" action="http://mywebsite.com/">
<input name="myparameter" value="12345" type="hidden" />
</form>
<script>
document.getElementById("myform").submit()
</script>
It's not a true redirect and it won't perform as well as a redirect, but this should work.
A "Redirect" is a GET. If you're doing a real direct what you're doing is essentially informing the browser to go to another url. The browser than makes another http call (using GET) to the new url. The url may contain parameters but it will always be a GET and not a POST.
What you could do is store some data in session and then when the second (redirected) request comes in, you can access these values from session.
Sorry, no. Redirects via POST are simply not supported in the HTTP spec. Most clients implementing the common kinds of redirects (301 - Permanent and 302 - Temporary) issue a GET request to the new location.

Rails page caching and flash messages

I'm pretty sure I can page cache the vast majority of my site but the one thing preventing me from doing so is that my flash messages will not show, or they'll show at the wrong time.
One thing I'm considering is writing the flash message to a cookie, reading it and displaying it via javascript and clearing the cookie once the message has been displayed.
Has anyone had any success doing this or are there better methods?
Thanks.
Cacheable flash do this:
in your application controller:
after_filter :write_flash_to_cookie
def write_flash_to_cookie
cookie_flash = cookies['flash'] ? JSON.parse(cookies['flash']) : {}
flash.each do |key, value|
if cookie_flash[key.to_s].blank?
cookie_flash[key.to_s] = value
else
cookie_flash[key.to_s] << "<br/>#{value}"
end
end
cookies['flash'] = cookie_flash.to_json
flash.clear
end
and then read "flash" cookie via Javascript and insert the message inside the HTML
I'm dealing with the same problem and I found cacheable-flash plugin that does exactly what KJF described in the question.
I think this is simpler and nicer solution than making excessive ajax calls.
One solution would be to cache the page, but include a javascript snippet that will make another small request just for the section you want to be dynamic. So the user will download the page fully, and then when javascript executes, it will pull down the dynamic page element.
I wrote a short blog post about this a while back.
http://chase.ratchetsoftware.com/2008/12/rails-caching-dynamic-fragments/
Also, Greg Pollack of RailsEnvy did a screencast where he focuses on having dynamic data in cached pages.
http://railslab.newrelic.com/2009/02/05/episode-5-advanced-page-caching
Hope this helps,
Chase Gray
You don't have to cache entire page. Try fragment caching API
Old question... but I got around this by including the flash message into my cache key.
caches_action :show, cache_path: proc { |c|
most_recent_update_time = MyClass.order('updated_at DESC').limit(1).first.try(:updated_at).to_i
{ tag: most_recent_update_time.to_s + c.flash.collect{|x| x}.join }
}
If you have flash messages on your show action this will obviously break the cache often, but works well if you aren't doing a lot of messages.
Unobtrusive Flash puts flash message into cookie, and displays it via JavaScript. It provides vanilla and Bootstrap flavored JS display logics. It works in normal and ajax requests. It is also easy to hook into frameworks such as AngularJS.
I don't use Rails but this is how I did it in Python using UUIDs:
# set flash messages like this
def flash(self, title, body):
session['flash_messages'].append({
'title': title,
'body': body,
'uuid': uuid().hex # stores a UUID as a string
})
...
self.flash('foo', 'bar')
Then in the base template I have this:
<script type="text/javascript">
{% for m in session.flash_messages %}
if(!Cookies.get('{{m.uuid}}')) {
Notify('{{m.title}}', '{{m.body}}');
Cookie.set('{{m.uuid}}', 'true', 86400); // key, value, expiry seconds
}
{% endfor %}
</script>
I'll break it down for the Pythonically-challenged:
When you add a flash message, you create a unique ID and store it with that message.
Before you display the message, you check to see if a cookie named with the message's unique ID has been set.
If that cookie has not been set, flash the message and set the cookie. Expire the cookie in a day, or as brief as you think is wise.
Now if this page is pulled from cache, it will be okay. At step 2 the test for the cookie will pass because it has already been set, and the message will not be displayed.

Resources