I need to switch the session store in my Rails 3 app from cookie_store to redis-session-store. There are many reasons for this (security, SSO with other Rails and non-Rails apps). Are there any best practices on how to do it without loosing all current sessions?
What i could imagine is a two steps approach:
Collect all user sessions for N days and store them in the DB or in Redis (update if already stored).
Use stored user sessions to create entries in Redis.
Alternatively, on the fly migration would also be possible. Means read cookies, use secret key to decrypt the session data and store it as a new session in Redis.
I realize this ticket is pretty old, but this may help others. We ended up changing our session store to Redis, but then still looking for the legacy cookie (for a week or two) before no longer respecting them.
There are probably some security concerns to consider before using this strategy - you want to make sure those risks are worth it compared to the cost of having to sign your entire user-base out all at once. With Rails, the cookies are encrypted and can't be tampered with.
Here's what we used:
class SessionsController < Devise::SessionsController
LEGACY_COOKIE_NAME = "_old_session_name".freeze
def new
return if detect_valid_cookie
super
end
private
def detect_valid_legacy_cookie
legacy_cookie = request.cookie_jar.encrypted[LEGACY_COOKIE_NAME].presence || {}
valid_user_id = legacy_cookie['warden.user.user.key'].try(:first).try(:first)
return unless valid_user_id
user = User.find_by(:id => valid_user_id)
return unless user
if sign_in user
request.cookie_jar.delete(LEGACY_COOKIE_NAME)
redirect_to root_path # or whever you want
true
else
false
end
end
end
Stolen from here:
http://www.happybearsoftware.com/almost-protect-yourself-from-cookie-session-store.html (the last two sections)
Basically, use this:
Rails.application.config.action_dispatch.cookies_serializer = :hybrid
Quote follows:
This will cause Rails to accept sessions serialized with Marshal and exchange them for sessions serialized with JSON.
After you're confident that all your users sessions have been converted to JSON, you can roll out another release that flips the config value to :json.
Note: If you're storing complex Ruby objects in the session and need them to be serialized with Marshal, you won't be able to use the JSON serializer.
Related
I am building an E-commerce website on ruby on rails from scratch.(This is my first project on ruby on rails)
My product belongs to a subcategory which in-turn belongs to a category.
My filters partial include multiple check-boxes for category,subcategory,additional_category(Like hand made clothes,factory built etc.),lifestyle(relaxed,corporate etc) and cloth_material_type(this has around 30 options)
I am sending 5 arrays for each of these cases to the backend to search through the associations.
Now when a non logged in user reloads the page the filters set by user resets to default.
To avoid this I have four options in mind.
Option 1. Store the filter values set by the user in the cookies which is fast.But it might slow down the user's browser.
Option2 . Store the values in a session using ActiveRecord::SessionStore gem which will increase the size of session for me to 65K but would slow down the application.
Option 3 .Using jquery modify/create document.url options so that every filter option gets appended to the document.url and on reload I get the parameters set by the user for filtering.But this looks very cumbersome to implement.
Option 4. Using gems like rails temporary database etc.
I have opted with option 2 and using session store for the purpose but I think that it will become cumbersome to maintain this in the future.
Just need some suggestions like what do other rails ecommerce websites do to solve this problem or is there any better way to solve this.
Redis
What I'd do is add a layer of abstraction; specifically I think you'd benefit from using Redis, or similar temporary db (as you alluded to in your question).
Redis is a key:value database, which basically stores JSON values for you to use within your app. If you tie it to a model, you'll be able to store temporary values without hindering your app's performance.
I think you could setup Redis to store a guest id, and an array of your values from that:
[guest_user_id] => [
1 => "x"
2 => "y"
3 => ["z", "a", "b"]
]
You'd be able to generate the guest_user_id when you initialize the Redis system, and store it in the user's session. This way, you're only storing minimal data inside your user's browser, and can populate the various controller actions with Redis data:
#config/routes.rb
resources :categories do
resources :subcategories
end
#app/models/user.rb
Class User < ActiveRecord::Base
def self.new_data
# create guest_id and send to Redis
end
end
This will allow you to populate a session with your guest_id if the user is not registered:
#app/controllers/products_controller.rb
class ProductsController < ApplicationController
def show
#user = user_signed_in? ? current_user : User.new_data
#You'll then be able to populate their Redis values with the data from the product selection etc
end
end
I could go into more specifics, but as you're only looking for suggestions, this is what I have to recommend at the moment
The scenario: I need to give models access to API tokens stored in the session.
Background: I have an API-driven rails 3 application utilizing DataMapper(DM) and a DM adapter to interface with the API. Each DM model has a corresponding REST-ish API endpoint much like you get with rails scaffolding. The API requires various headers for requests, including API tokens, keys, ids etc. The headers have nothing to do with the requested data, they exist for authorization and tracking purposes only. A number of these tokens are stored in the session. I want a clean way to make these API headers available to any model during a request.
Possible solutions:
1. Passing session variables from the controller to the models
The obvious answer is passing the tokens in a hash or other object from the controller to the models. A controller action might have the following: #user = User.find(params[:id], api_headers).
The problem is needing to override any model method to accept the additional api_headers object. Not counting methods defined by Rails and DataMapper, there are hundreds of methods already defined in the application models that would need to be rewritten. So I'm ruling out a rewrite, and this also doesn't seem like a good solution since it would require overriding a ridiculous number of DM methods like the User#find example above.
2. Some metaprogramming hack
I could catch any ArgumentError's on DM's base class and check if the last argument is the api_headers object, then set the values as instance variables and invoke the requested method. This thought exercise already has me cringing at dealing with optional arguments etc. If given long enough I could probably create a functional Frankenstein that should get me fired but probably wouldn't.
3. Use a singleton (current preferred solution)
In the application controller set a before_filter to dump the session-stored API headers into a singleton ApiHeaders object. Then any model making an API request can get that singleton with the required API headers.
An additional after_filter* on the application controller would set all attributes to nil on the ApiHeaders singleton at the end of the request to prevent leaking headers between requests.
This is currently my preferred solution but I don't like that the API header values could potentially carry over into other requests if the after_filter doesn't get invoked. I don't know in which scenarios this might happen (in an application error perhaps?) which raises concerns. All I know is the values don't necessarily die with the request.
4. Custom code
Drop support of DataMapper and the custom API adapter and manually make all API calls, passing through all required API headers. Besides the fact I don't have time for this level of rewrite, why use a framework at all if you have to throw a huge chunk out to support a custom auth scheme?
Summary
What's the cleanest way to get these pesky API tokens from the session into the bowels of the application where they can be sent with each API request? I'm hoping for a better solution than those listed above.
* An alias for after_action
I set the current user and the request information on my User model using the request_store gem which is just a tiny shim over thread local storage with a bit of clean-up.
This makes the information available from any of my models via the User class. I have User.current, User.request and User.location available wherever I need it.
Your controller just has to set User.current and User.request once it has authenticated the user.
Example User model:
# models/user.rb
require 'request_store'
class User
def self.current
RequestStore.store[:current_user]
end
def self.current=(user)
RequestStore.store[:current_user] = user
end
def self.request
RequestStore.store[:current_request]
end
def self.request=(request)
# stash the request so things like IP address and GEO-IP based location is available to other models
RequestStore.store[:current_request] = request
end
def self.location
# resolve the location just once per request
RequestStore.store[:current_location] ||= self.request.try(:location)
end
end
Use Thread.current, which is passed in from request to model (note, this breaks if, inside your request, you use sub-threads). You can store the attribute you want to share in a cattr_accessor or in rails cache:
in a cattr_accessor
class YourClass
cattr_accessor :my_var_hash
...
# and in your controller
# set the var
YourClass.my_var_hash = {} if YourClass.my_var_hash.nil?
YourClass.my_var_hash[Thread.current.object_id] = {}
YourClass.my_var_hash[Thread.current.object_id][your_var] = 100
... and in your model
lvalue = YourClass.my_var_hash[Thread.current.object_id][your_var]
Note, if you use this method, you will also want to make one of the hash values a timestamp, and do some housekeeping on getting, by deleting old keys, b/c you'll eventually use up all your system memory if you don't do the housekeeping
with cache:
# in your controller
#var = Rails.cache.fetch("#{Thread.current.object_id}_var_name") do
return 100 # do your work here to create the var value and return it
end
# in your model
lvalue = Rails.cache.fetch(("#{Thread.current.object_id}_var_name")
You can then set the cache expiration to 5 minutes, or you can wildcard clear your cache at the end of your request.
UPDATE: All, thanks for the responses - here is some more significant info.
I'm using a Neo4J graph database for the back-end, ROR 3 (MRI), and one single Neo4J database server accessed via REST.
If you don't know much about Neo4j, to use more than one database server (master/master) for data costs $26,000, which means I have to code for optimization now, not later, or come up with $26k...I'm sure you can guess which way I'm going with this..And I'm using it via rest, not locally etc, so performance matters...
I have one database server that has to handle all of the database work, and yes 1 ms counts under this scenario where some other queries take up to 40 ms. So no, I don't want to hit the database unnecessarily as it will simply add unnecessary work to it.
It might be easy to say "don't code for optimizations or problems you don't have yet" yet given the bottleneck and steep costs - and the fact I already have what I need done except for the authentication piece, it really doesn't apply.
What I simply wanted to know, was if the #current_user ||= is valid across pages.. The answer is that it's valid in a request, and not across them or pages. Yes this is a simple question, but sometimes they have to be asked in the midst of R&D and advanced stuff. Hence my gut feeling to stick with sessions to hold the id of the user logged in.
Thanks for your help!
I'm trying to allow a user to login to my site either by cookies or by username and password. The username/password part works fine, but having a problem with introducing cookies....
I've ready plenty of "how tos" including: http://ruby.railstutorial.org/chapters/sign-in-sign-out
I do not want to use a gem, devise, etc.
PROBLEM:
I notice that requests from the page to read current_user (a helper in the application controller) results in database reads, even though I'm using ||=..
I've tried the #current_user ||= User.find by blah blah blah
and it ALWAYS hits the database. This shoudn't be the case right? It should hit once and then that's it, correct?
But before you suggest any tutorial - I've seen tons of them - here is my question.. is #current_user saved across pages, or just for the current page? The link above mentions it only saves it for the current page...
You see, I don't want to keep hitting the database needlessly to find out the same person previously is logged in.. I can do that with session variables.
I really just want to check for a cookie,and would be happy to keep on using session[:user_id] after that.. for performance reasons, I do not want to keep hitting the database.
Any help?
My NEW code is below (and this too always hits the database as it should in this instance). I removed the typical #current_user ||= find_by.. because it was useless - it was always hitting the db.
.. I already tried https://github.com/cliftonm/basic-auth and http://ruby.railstutorial.org/chapters/sign-in-sign-out etc..
(ROR 3.2...)
class ApplicationController < ActionController::Base
protect_from_forgery
#before_filter :set_var
helper_method :current_user
private
def current_user
#current_user = User.find_by_id(session[:user_id]) #if session[:user_id]
if (#current_user)
return #current_user
end
#current_user =User.find_by_remember_token(cookies[:auth_token]) if cookies[:auth_token]
if (#current_user)
return #current_user
end
end
User.find will always hit the database unless you have some kind of cache extension loaded. Rails.cache can be configured several ways, but the most popular, if this sort of thing is required, is Memcached.
Unless you're dealing with massive scaling issues, the time required to fetch a user record should be less than 1ms, so it's hardly worth fussing about. Check your log/development.log to see the execution times of your various queries and focus first on the slowest ones.
Instance variables like #current_user persist only for the duration of the request. Remember that the HTTP protocol is stateless, each request exists independent of the others, and the only way to communicate state is via cookies, a persistent store like a database, a temporary store like an in-memory cache, or by parameters sent in with the request itself via GET or POST.
If you want to save something across pages, add it to the session, but be careful. You should only be persisting things like strings, numbers, or booleans. You should not be adding models. Further, using the default cookie store, each thing you put in the session increases the overhead on all requests made to your application from that point forward until you remove that from your session store.
Don't sweat the little things until you've got all the other problems solved. Business logic first, optimization second.
Neither of these should be slamming the database every time. Please let me know.
if (#current_user)
#current_user
else
#current_user = User.find_by_id(session[:user_id])
end
or
#current_user ||= User.find_by(id: session[:user_id])
Have you tried using a before_filter (instead of a helper) to load current_user only once? You would then store it / access it using an instance variable.
class ApplicationController < ActionController::Base
before_filter :load_current_user
def load_current_user
#current_user = # your code goes here.
end
end
Edit:
My bad. How about storing a server-side encrypted current_user record in the cookie, and keeping the hashsum in the session for later checkup?
I am running Ruby on Rails 3 and I would know if the code that I am using in order to set the cookie value for user authentication purposes is strong enough.
In my model I have:
require 'digest'
class User < ActiveRecord::Base
...
def make_cookie_id_salt(string)
secure_hash("#{self.id}--#{string}")
end
def secure_hash(string)
Digest::SHA2.hexdigest(string)
end
end
In my controller I have:
cookies.signed[:current_user_id] = { :value => [#user.id, #user.make_cookie_id_salt(#user.id)], :expires => 15.days.from_now }
Is it strong enough? If no, how I can improve that (make an example!)?
Everything that gets put into cookies is stored as plain text.
If you set a cookie, and then check the cookies in your browser you will notice (in your case the cookie name would be current_user_id) that it is represented by a string of characters like: G8gcm9sbCB5b3VyIG93biBhdXRoIHRvIGt... (Not quite plain text, right? It is actually Base64 encoded, but you can easily read it - require('base64'); Base64.decode64(string)).
Rails stores a special _yourapp_session cookie that is used to check the cookies validity. If for example someone/something was trying to modify it, it would get rejected.
Now in your case it doesn't really matter if you try to hash something in the cookie or not.
It is just used for authentication (to look up a user in the database by his id) and you are not storing any unique secret data (Which you should not place in a cookie anyway, but it would be the only reason to hash something)
Of course someone could steal the cookie of a user (if he used a public computer and hasn't cleared his cache, etc.) and log in, but there's no way to prevent that (No matter what kind of hashing was used to obfsucate it)
In conclusion you should be fine with what you have.
Rather than try to create your own, I suggest using the Authlogic gem. In a few minutes of configuration you get a complete authentication solution, including cookies and much more. If you really want to roll your own, install the Authlogic gem and take a look at how they do it.
Devise is another option. It's extremely configurable, pretty DRY, with exhausting wiki.
For now-days I prefer it over Authlogic.
WARNING: Complete newbie to RoR and Ruby alert! *
I have a login method that looks like this:
#user = Person.find(:first, :conditions => ["email=?", params[:email]])
if #user and #user.password==params[:user_password]
session[:user] = #user
else
flash[:warn] = 'Invalid password!'
However, the user record can get very large, so I don't want to store the entire user record in my cookie session.
How can I modify this code so that a specific field does not get stored in the session? There are two fields that can get very large (very large user profile data) and will not fit within the cookie session 4 kilobyte limit, so I want to exclude those from being stored in the session.
I would do :
session[:user] = #user.id
And then create a before_filter going something like this:
before_filter :get_user
def get_user
#user = User.find_by_id(session[:user])
end
edit: This is not exactly what you were looking for, but if you can't store all the object in the session variable you might want to consider this option. It's only one request so it won't be too resource intensive. Plus like this you can check at every page load that the user exists and this might be helpful, security wise.
The design of Rails often serves to steer you in a very specific direction. In this case, the fact that by default Rails' sessions are stored in a cookie is a strong hint that you shouldn't be storing large objects in the session. By all means store the User ID, but not the User object itself.