Rails 4 session is not updated between requests - ruby-on-rails

Let's say we have a very simple controller's show action:
def show
session[:shown_counter] ||= 0
session[:shown_counter] += 1
puts "session id: #{session.id}"
puts "shown #{session[:shown_counter]} times"
end
When I hit the url that invokes this action from my browser(chrome), it works as expected and I see the shown_counter increment. But I have a flash that makes some (5-10) requests to the correct URL, invokes my show action, but I can see that the counter is not increased although the session id is always the same.
When I reload the flash, I'll see that the counter increased by 1, but get "stuck" until I refresh the flash again or I make a "regular" request (with the browser).
How does it work? Why the counter doesn't grow with the flash requests?

I believe your Flash app needs to get the session cookie as provided by your Rails app server in a Set-Cookie header, store the data provided, and pass this key-value pair as a Cookie header on its next request.
(Save the _myapp_session=Xyz(etc.) part from each response, in other words, and pass it as Cookie on each subsequent request.)
Otherwise, Rails won't know to which session your request belongs.

After talking to the flash creator, I think I got an answer.
The requests are asynchronous, that means a request won't wait for a response => the cookie won't be updated => the session (that uses the cookie store) won't be updated as well.
I guess this is the scenario, please tell if it makes sense to you and if you agree / disagree.

Related

Can I safely tell Rails to remember the result of an expensive instance method?

My current_user has a has_privilege? method which calls the database to check whether this user has a certain admin privilege or a superceding one (system is similar to Wordpress privileges). As has_privilege? gets called several times per page, can I tell Rails to remember the result for as long as the instance exists, using the method explained at How can I save the value of an instance method?, or would this make the app insecure? It's a bad idea to put this information into the session variable or cache, right?
as long as the instance exists
It depends what you mean by that.
Rails treat each HTTP requests in a separated process so you can cache the has_privilege? method output for the current request (so calling many times the method will query the DB only the first time), but on the next request, it will be executed again and then cached (and actually you want that as you may change the permissions and don't want the user to keep the old permissions).
In order to do so you can use this simple trick:
class User < ActiveRecord
def has_privilege?
#has_privilege ||= begin
# You code here. The returned value will be cached in #has_privilege.
end
end
end
So the first time the method is called, #has_privilege is nil for your instance of the user, so the || will its right side part which is the = begin ... end.
Excepted if the code return nil, the value will be assigned to #has_privilege.
On the next call, #has_privilege is no more nil, therefore || will no trigger its right side part and return immediately.
I need a cross request cache
In this case, you have to go with JWT and save the value in the token.
JWT is a token generated and signed by the server, and it must be sent back in to each requests, otherwise the server will reject it.
As only the server can sign the token, in the case the user tries to change it, the server will reject and sign out the user.
You can read more on their website, it's quite simple to use.

Rails 4 external redirection and sessions issue

I am trying to build a website in Rails 4 to track users redirects and site element views.
I decided to use session ids which I believe are quite unique in the short term but I'm having a strange issue.
Example procedure:
user follows a redirect, the system stores this action with a Session ID, let's say xxx
user reaches destination page, which contains a tracker, the system stores this action with ANOTHER Session ID, yyy
user reaches another page which also contains a tracker, the system stores this action with Session ID yyy
After the second action is stored, the session ID stays the same yyy for every request after that, but I need to have the same session ID every time.
In session I also store a SecureRandom.hex generated code, which also changes from the first to the second request (which is not a surprise, since the session ID changes).
I also tried using a cookie, same result.
Please notice that these redirects are external, but all the requests are then made to the same domain (exactly the same, without www and in https).
Any idea?
Thanks in advance.
Update
this is the source code responsible for managing redirects:
before_action :load_redirect, :only => [:http_redirect]
def http_redirect
raise ActionController::RoutingError.new('Redirect has been disabled') unless #redir.enabled
ua = UserAction.create(
:session_id => session.id,
:user_agent => request.user_agent,
:trackable => #redir,
:ip_address => request.remote_ip,
:referer => request.referer
)
redirect_to #redir.destination_url
end
private
def load_redirect
#redir = Redirect.find(params[:id])
end
UPDATE:
Since you are using an iframe (per comment discussion below) for tracking code, the issue is likely that on the external site cookies are not being passed from parent page to the iframe because the iframes origin (domain) is different from the parent page.
OLD ANSWER:
(Still could be helpful for others debugging similar issues)
Source code would help. Without that, here are a few things to try:
Try disabling CSRF protection for the external tracking link action (I'm assuming it POSTs or PUTs data from an external source). CSRF protection could be creating a new or null session for those requests. Put this in the controller that contains the action accepting data from the external source:
protect_from_forgery :except => [:your_action]
The redirect (especially if it's a 301) could be cached in the browser you are using, hence having a different cookie and session than the request your tracking code makes. The stale cookie would be part of the cached redirect.
Try putting cache control headers on your controller action that does the redirect.
response.headers['Cache-Control'] = 'no-cache, no-store, must-revalidate'
response.headers['Pragma'] = 'no-cache'
response.headers['Expires'] = '0'
Your browser may not support setting cookies on a redirect, or possibly third-party cookies. Try in a different modern browser?
There could be a bug in your code. If these solutions don't work, maybe post it?

Detecting Rails 4 Session cookie tampering

Background
I'm an experienced web developer (mostly with Python and CherryPy) who has implemented secure session management from scratch before, and is now learning Rails. I'm investigating the behavior of Rails sessions as exposed by the session object that is available in the ActionController instance and view contexts.
Question/Problem
I have read that the default implementation of sessions in Rails 4 uses an encrypted and tamper-proof cookie. Cool, I guess that means I can use it to hold a user ID for user sessions without worrying about session forging (tamper-proof) or anyone being able to find out what their ID is (encrypted). I wanted to test this and see what rails would do if the session cookie was altered.
So, I went and altered the content of the session cookie attribute using a browser add-on, and when I reload the page with the new cookie value, Rails just happily gives me different new values for session_id and _csrf_token.
What happened to session cookie integrity!?
Shouldn't rails detect (via HMAC signature) that the cookie was altered and then tell me about it somehow?
I'm terrified that I'm missing something obscenely obvious, but I've been having no luck searching for an answer on the web, and the source code isn't giving it up easily either (I'm new to ruby). Thanks in advance.
The Experiment
I created a new app and generated a controller with an index action:
$ rails new my_app
$ cd my_app; rails g controller home index
Then I added these two lines to the app/views/layouts/application.html.erb file:
<%= session.keys %><br/>
<%= session.values %>
I started up the dev server and navigated my browser to "localhost:3000/home/index". As expected, the page has the following lines at the bottom:
["session_id", "_csrf_token"]
["8c1558cabe6c86cfb37d6191f2e03bf8", "S8i8/++8t6v8W8RMeyvnNu3Pjvj+KkMo2UEcm1oVVZg="]
Reloading the page gives me the same values, although the app sets a new value of the _my_app_session cookie attribute every time. That seems weird to me, but I'm getting the same session hash values, so I guess it's cool.
Then, I used a cookie editing add-on for Chrome to alter the value of the _my_app_session cookie attribute (replacing the first character of the attribute value). Reloading the page shows completely different values without anything happening. WAT?
I can't claim a really thorough understanding of the code here. But I can tell you this much:
I followed your steps exactly (using Ruby 2.0.0-p247 & Rails 4.0), with one exception -- I also added the 'byebug' gem to my Gemfile and inserted a debugging breakpoint in the HomeController#index action.
From the byebug console, at that breakpoint, I could see the unedited session cookie via:
(byebug) cookies["_my_app_session"]
"cmtWeEc3VG5hZ1BzUzRadW5ETTRSaytIQldiaTMyM0NtTU14c2RrcVVueWRQbncxTnJzVDk3OWU3N21PWWNzb1IrZDUxckdMNmZ0cGl3Mk0wUGUxU1ZWN3BmekFVQTFxNk55OTRwZStJSmtJZVkzVmlVaUI2c2c5cDRDWVVMZ0lJcENmWStESjhzRU81MHFhRTN4VlNWRlJKYTU3aFVLUDR5Y1lSVkplS0J1Wko3R2IxdkVYS3IxTHA2eC9kOW56LS1IbXlmelRlSWxiaG02Q3N2L0tUWHN3PT0=--b37c705a525ab2fb14feb5f2edf86d3ae1ab03c5"
And I could see the actual encrypted values with
(byebug) cookies.encrypted["_my_app_session"]
{"session_id"=>"13a95fb545a1e3a2d4e9b4c22debc260", "_csrf_token"=>"FXb8pZgmoK0ui0qCW8W75t3sN2KLRpkiFBmLbHSfnhc="}
Now, I edit the cookie by changing the first letter to "A" and refresh the page:
(byebug) cookies["_my_app_session"]
"AmtWeEc3VG5hZ1BzUzRadW5ETTRSaytIQldiaTMyM0NtTU14c2RrcVVueWRQbncxTnJzVDk3OWU3N21PWWNzb1IrZDUxckdMNmZ0cGl3Mk0wUGUxU1ZWN3BmekFVQTFxNk55OTRwZStJSmtJZVkzVmlVaUI2c2c5cDRDWVVMZ0lJcENmWStESjhzRU81MHFhRTN4VlNWRlJKYTU3aFVLUDR5Y1lSVkplS0J1Wko3R2IxdkVYS3IxTHA2eC9kOW56LS1IbXlmelRlSWxiaG02Q3N2L0tUWHN3PT0=--b37c705a525ab2fb14feb5f2edf86d3ae1ab03c5"
(byebug) cookies.encrypted["_my_app_session"]
nil
So the session is nil at this point in the request:
(byebug) session
#<ActionDispatch::Request::Session:0x7ff41ace4bc0 not yet loaded>
I can force loading the session with
(byebug) session.send(:load!)
and when I do, I see that the resulting session id is
"f6be13fd646962de676985ec9bb4a8d3"
and sure enough, when I let the request finish, that's what I see in the view:
["session_id", "_csrf_token"] ["f6be13fd646962de676985ec9bb4a8d3", "qJ/aHzovZYpbrelGpRFec/cNlJyWjonXDoOMlDHbWzg="]
I also have a new cookie value now, unrelated to the one I edited.
So from this I think we can conclude is that what's happening is that since the cookie signature could not be verified, the session was nullified and regenerated. I now have a new session, with a different csrf_token.
The relevant code appears at actionpack/lib/action_dispatch/middleware/cookies.rb:460-464, in the EncryptedCookieJar class:
def decrypt_and_verify(encrypted_message)
#encryptor.decrypt_and_verify(encrypted_message)
rescue ActiveSupport::MessageVerifier::InvalidSignature, ActiveSupport::MessageEncryptor::InvalidMessage
nil
end
Rather than decrypting a message with an invalid signature, we just treat it as nil. So the unverifiable cookie that stores the session id and csrf token is not used to load the session, and anything that depends on the values in the cookie will fail.
So why didn't we get an error rather than just a new session? That's because we didn't try anything that depends on the encrypted values. In particular, although we have
protect_from_forgery with: :exception
(as opposed to :null_session) in ApplicationController, Rails does not verify the csrf token on GET or HEAD requests -- it relies on the developer to implement these actions according to spec, so that they're non-destructive. If you tried the same thing on a POST request, you'd get an ActionController::InvalidAuthenticityToken error (as you can easily verify for yourself).

messaging from the model level to controller level for ajax purposes

I have a page called /examples/1/duplicate
on there is an ajax form which kicks off /examples/1/run_duplicate
the controller method for run_duplicate runs Example.duplicate(session)
I pass in the session, which I understand is a holy no-no.
the duplicate model code is roughly:
def duplicate(session)
session[:duplicate] = 0
duplicate_foobars
session[:duplicate] = 1
duplicate_snafus
session[:duplicate] = 2
duplicate_widgets
session[:duplicate] = 3
end
I have another controller method on the duplicate page which is doing a long poll: /examples/1/check_dupe_status
The purpose was to get the updated status from session[:duplicate], tweak a progress report,
and report it back to the user via ajax so they could see the progress of Example.duplicate() .
sessions did not update the way I hoped. I can see the code run, but the sessions don't update, so /examples/1/check_dupe_status never knows that anything has kicked off.
Given that I did this wrong from the start, what is the correct way to notify a user about the state of Example.duplicate() ?
Sounds like you've got two separate requests, with one writing to the session and one trying to read from the session concurrently. Is that correct? Stop reading if it's not.
This can not work because the "session" is just a cookie - a header on the HTTP response that's downloaded to your browser as a cookie, then re-uploaded on the next request, then re-downloaded on the next response, ad nosium. In your code, here is the order of operations:
/examples/1/run_duplicate writes "0" to what is essentially a Ruby Hash, representing your session cookie
/examples/1/check_dupe_status reads the values from the session cookie that was just sent from your browser with this request. It probably didn't have anything at all in :duplicate, so it will appear as blank.
/examples/1/run_duplicate writes "1" to the Ruby session Hash
/examples/1/check_dupe_status reads, again, the session cookie which that request sent - nothing has changed
/examples/1/run_duplicate writes "2" to the Ruby session Hash
/examples/1/check_dupe_status reads from the session cookie it originally sent - no change
/examples/1/run_duplicate writes "3" to the Ruby session Hash and the request finishes, sending the session back as a cookie with a value of 3 at :duplicate.
/examples/1/check_dupe_status is still sitting there like a dufus, reading the blank session cookie it originally sent
At some point, /examples/1/check_dupe_status may timeout, and it may return with the session cookie. But guess what? Since :duplicate was never set in that session cookie, it will overwrite the one in your browser, and :duplicate will be blank in the next request you send.
Hopefully I expressed that clearly. Basically you're hitting a race condition in your cookies, and that's very difficult to overcome if you're sending concurrent requests from the same browser.
The best way to handle this would be to write your duplicate 0,1,2, etc. values to some database table. Your long-polling request could then just read out of the database. A little less efficient perhaps, but of course it has the advantage that it could work.

Setting Session Variables with AJAX and Rails when appcache is present

Have an ajax call to "updateUser" which does this:
puts session[:user_id]
user = User.find(params[:user_id])
if user
session[:user_id] = user.id
session[:user_name] = user.first_name + " " + user.last_name
puts session[:user_id]
render text => "Success.
end
The first puts shows the original user_id and the second shows the new user_id, so it would appear to be working properly. However, when I navigate to another page, all the session information is still that of the original user_id. What have I done wrong?
I have a feeling it has something to do with the local session cookie not being updated.
UPDATE
Definitely has something to do with caching. I can go to the page, clear the browser cache (am using Chrome as my browser), then run the ajax call and it works properly once. After that I am locked in to the (new) old user again.
UPDATE 2
Looks like it is something specifically to do with html5 application-cache. If I kill the appcache or run the script from a page that does not include manifest it works just fine. Still can't get it working properly on the cached page.
The same session id is being sent to the server from the cached page as the non-cached page, and the response headers are identical. But each request from the locally cached page causes the server to start with old session information.
http://diveintohtml5.ep.io/offline.html
I can tell that you've got a manifest caching problem, and altering the session itself is not going to clear the manifest. The cache is persistent until such time as the cached item is de-cached or the manifest is invalidated.
Another user ran into this same issue in a different way: they passed their session data in the URI and ended up caching a new application each time the user visited. Their solution may be useful:
How to clear Application cache (HTML5 feature) using JavaScript?
You might also take a look at this, on the various storage caches:
http://sharonminsuk.com/blog/2011/03/21/clearing-cache-has-no-effect-on-html5-localstorage-or-sessionstorage/
And finally, a resource on updating a cache file with JS:
http://www.html5rocks.com/en/tutorials/appcache/beginner/
This last one I would use after checking if the session ID has changed: update the session ID, then confirm the change, then clear and re-download the cached files.
Good luck. I hope that helps some.
The problem is that the session information is stuffed inside the application cache somewhere, All requests sent to the server are sent using that session info (which was cached on page load). So, we need to update the application cache with window.applicationCache.update() after the successful ajax call. This will cause the next request sent to the server to have the updated session information and all is well.
$.ajax({url: "/contoller/update_logged_user",
data: {id: user_id},
success:function(){
window.applicationCache.update();
}})
I encountered a very similar problem... had some code to store a user's zip code in session[:zip] when provided with an ajaxSubmit'ed form. Modified the implementation only slightly and suddenly session[:zip] had amnesia. Storing the info in cookies[:zip] worked properly. Path of least resistance.
Would you try setting the session[:user_id] = nil before assigning it with another value user.id and see what happens?

Resources