I've built a multi page form wizard for a single model in rails, as a user enters data into the form regardless of the wizard page, the data is saved in session parameters. All works great, until the user finally submits the form, then I want those session parameters to also be cleared out.
I currently am removing the session by its attribute name and then resetting the session. This appears to work, but then if the page is refreshed, the original session will return. I have cleared the cache, and cookies but it seems like this is caused by the browser saving the session somewhere that the delete and rest are not catching.
In my controller after the new object is saved I call this to clear the session
session[:design_attributes] = nil
reset_session
redirect_to :controller => 'designs', :action => 'index'
Am I missing something, that also needs to be removed from the client side session? I would expect that the user, after hitting the above code should never be able to refresh their browser and see the session variables repopulated.
Related
One of the form pages on our website is showing, occasionally, inconsistently, a fair number of ActionController::InvalidAuthenticityToken errors.
Viewing the logs we can see this occurs when:
a) user visits form (works)
b) clicks an option (works)
c) clicks another option DAYS later (causes CSRF error)
We believe the issue is that after (b) the user quits their browser, which erases session cookies, then when they re-open their browser if they have "reopen pages" enabled on their browser it re-displays the page, but there is no session and therefore the expected CSRF token is missing from their session.
We've added some code to handle that particular CSRF error with a custom error message.
We'd like to TEST the new code in rspec by simulating the same sequence of events.
That requires a way to erase Capybara's session for that user, simulating what happens if browser is closed then reopens:
visit form_url
click_button button_a
?? erase session cookie, with page object still 'open', so form object still exists??
click_button button_b
Using reset_session! wipes the page object too.
In an Rspec spec, how can we erase/invalidate the "browser" session cookie so when we click another button on the form, a Rails CSRF error will be triggered?
I think there is a simple way to do this that involves making a request to your Rails backend. The high level is that you expose a route and a controller action to test mode that you do not expose in any other environment.
Make the reqeust to this endpoint in the specific situation you want to clear the session cookie.
In the controller action, you clear the session cookie and and set this so that the request you have just made is not going to set a new session
# config/routes.rb
get 'clear-session', :to => 'application#__clear_session' if Rails.env.test?
and
# controllers/application.rb
if Rails.env.test?
def __clear_session
request.session_options[:skip] = true
cookies['__yourapp_session'] = { :value => '', :expires => Time.at(0) }
end
end
In Rails, I have implemented the below code for user auth (confirmed to be correct). However, I wanted to confirm my thinking for this strange session[:session_token]. is this the "cookie" that is stored in the browser?
class ApplicationController < ActionController::Base
protect_from_forgery with: :exception
helper_method :current_user, :signed_in?
private
def current_user
#current_user ||= User.find_by_session_token(session[:session_token])
end
def signed_in?
!!current_user
end
def sign_in(user)
#current_user = user
session[:session_token] = user.reset_token!
end
def sign_out
current_user.try(:reset_token!)
session[:session_token] = nil
end
def require_signed_in!
redirect_to new_session_url unless signed_in?
end
end
My understanding so far of how this works is that whenever the browser/client sends a request to rails, the cookie (with the session[:session_token]) is also sent over, thus allowing the current_user method to find the user. Is my understanding correct? This is strange to me because there's a gap of knowledge of how exactly the browser/client gets access to the session cookie when we declare it in ApplicationController (Rails-side).
You are pretty much there. Although, I have a feeling you might be confusing apples with oranges...
Sessions:
Very often in dynamic web sites one would want to store user data between HTTP requests (because http is stateless and you can't otherwise associate a request to any other request), but you don't want that data to be readable and/or editable on the client-side inside of the URL (like.. yourwebsite.com/yourPage?cookie=12345&id=678), and so on..., because you don't want the client to play around with that data without passing through your server-side code.
One way to solve this problem is to store that data server-side, give it a "session_token"(as you called it), and let the client only know (and pass back at every http request) that token. This is how the session is implemented.
Cookies:
The most common technique for implementing sessions in Rails involve using cookies, which are small pieces of text placed on the user’s browser. Because cookies persist from one page to the next, they can store information (such as a session_token or whatever else you want) that can be used by the application to retrieve the logged-in user from the database.
Where is the Session Stored in Rails?
Using both of the above concepts I can now tell you that the default session store inside of Rails is CookieStore, which is about 4KB in size.
To put it simply...
def sign_in(user)
#current_user = user
session[:session_token] = user.reset_token!
end
...method that you defined places the user into a temporary session.
Then the idea is that the following...
def current_user
#current_user ||= User.find_by_session_token(session[:session_token])
end
...method would find and retrieve the user from the database corresponding to the session token and initialize it to a variable you specified.
Additional info:
You should also note that there is an important difference between Rails's session and cookies helper methods...
They both generate cookies, however, session[...] method generates temporary cookies, which should expire upon the browser exit, and cookies[...] method creates persistent cookies, which do not.
Additionally, I would suggest having a look at Section 2 of Ruby on Rails Security guide. You might find it useful.
Hope this helps you out.
Session is stored in server side. And,
Cookie is stored in client side (in browser cookie). And,
When client/browser send a request to rails server, every time cookies are sent to rails server.
When a session is set in rails server, like: session[:user_id] = 4,
Rails store it in server side.
Session is saved in server side like key value pair (like json object)
For each browser, Rails set a session identifier in cookie, so that, Rails can find the correct session information for a request.
Without session identifier in cookie, Rails do not know, what session belongs to what browser.
So, session will not work without cookie.
Edit: Explain: sessions are stored server side
Suppose, I am using your web application, and after login I will be redirected to home page.
I open login page, input username and password, and click login button.
The form is submitted to sessions#login action.
in sessions#login - you check username and password - and set session[:session_token]:
if username and password is correct
random_unique_identifier_string = #user.remember_token
session[:session_token] = random_unique_identifier_string
redirect_to root_url
end
When server run this code session[:session_token], server need an unique identifier for each browser session.
So, server generate an unique identifier for this browser, such as: abc123
Server set all session variables in a place (may be in some folder or in database), label this folder as abc123.
Now server send a cookie request to browser - to set cookie _ebook_session = abc123.
(I see, if my app name is ebook, in rails, cookie name is like: _ebook_session)
Now the page redirect to home page.
** Note: Everything above happen in single request **
Now, in my browser, I want to open some page that need authentication (suppose, dashboard page).
You added before_action: require_signed_in! in dashboard controller.
So, when I open dashboard page in my browser, browser by default send all cookies with every request. so _ebook_session cookie is sent to server. Your server gets the value of _ebook_session cookie is abc123. Now your application know we need to look in abc123 folder for session. Now you can get value of session[:session_token] from abc123 folder.
** I have explained second request above **
Each browser needs unique session identifier.
Important: _ebook_session cookie will be set in browser in first request. If we already have _ebook_session cookie set in a browser, we do not need to set it again, second, third and next requests in that specific browser.
I hope, you understand.
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?
I am implementing a payment gateway in my app.
Its like this:
The user fills the form with necessary details, along with a field containing return_url(say http://myapp.com/ebs_payment/ebs_response?dr={somedata}) and submit the form to a secure payment site. After the transaction is complete, the secure site puts some encrypted data into my param {dr} and the user is redirected back to the return url. The problem here is, when the user returns to the app with the return_url, the application fails to pick up the session data and returns a nil value.
Before submitting the form, I put the object #fdPaymentDets in to session.
Here is my controller:
class EbsPaymentController < ApplicationController
#before_filter :login_required,:check_user_authenticate
#access_control [:ebs_response] => ('user')
def ebs_response
#fdPaymentDets = session["fd_payment_details"]
#deal = Deal.find(#fdPaymentDets.deal_id)
#categories = find_all_categories
end
private
def find_all_categories
#sp = SitePreference.find(:first)
Category.find(:all,:limit => #sp.categories_display_limit,:order => 'position')
end
end
When the user is redirected to the return url (http://myapp.com/ebs_payment/ebs_response?dr={encrypted_data}) from the secure site, rails is not picking the #fdPaymentDets object from session and making it nil thus resulting in an error when accessing data from the nil object.
The surprising thing is that, when I put the same return_url in my browser by hand, the session data is neatly picked and everything goes well.
Am missing any thing here? What could be the obvious reason?
Any help appreciated.
Cookies and redirects are messy and bug prone (from a browser's implementation perspective).
Take a look at
Safari doesn't set Cookie but IE / FF does
Suggestion would be to change the implementation to set the session first in the show action, and then update the value before the redirect
I am using ruby on rails and have things on my site that users can click to save and they are redirected to a page with both a login and a signup so that the user can continue with either option and save the content. The creates a problem for showing the proper user validation errors, b/c I need to use a redirect_to users/new in order to pass the params with the object id that the user is saving and if I use render :new, the errors are displayed but the object id is lost. Anyone have any solutions for this?
Store the original item id in the session, proceed with your normal login/signup process, when that completes, if there is a save item in the session, redirect to the action that handles the save (it can now grab the item id from the session and proceed).
"Out of curiosity, what is wrong with saving the object itself in the session? That way I wouldn't have to perform a second database lookup to find the object again." --TenJack
(this should probably be a new StackOverflow question)
Saving an item in the session is a Bad Thing - because the moment you migrate your model object (eg to add a column or something similar), the data in the session is now no longer a valid object of the model's type. eg it will still have the old attribute-list instead of the new one... and it will appear as an invalid object.
This is why it's best to store only the id - because you will fetch a fresh, correctly instantiated object from the db.