The Rails 4 Way (2014)
Chapter 13. Session Management
I’d hate to wake up some morning and find out that you weren’t you!
—Dr. Miles J. Binnell (Kevin McCarthy) in Invasion of the Body Snatchers (Allied Artists, 1956)
HTTP is a stateless protocol. Without the concept of a session (a concept not unique to Rails), there’d be no way to know that any HTTP request was related to another one. You’d never have an easy way to know who is accessing your application! Identification of your user (and presumably, authentication) would have to happen on each and every request handled by the server71.
Luckily, whenever a new user accesses our Rails application, a new session is automatically created. Using the session, we can maintain just enough server-side state to make our lives as web programmers significantly easier.
We use the word session to refer both to the time that a user is actively using the application, as well as to refer to the persistent hash data structure that we keep around for that user. That data structure takes the form of a hash, identified by a unique session id, a 32-character string of random hex numbers. When a new session is created, Rails automatically sends a cookie to the browser containing the session id, for future reference. From that point on, each request from the browser sends the session id back to the server, and continuity can be maintained.
The Rails way to design web applications dictates minimal use of the session for storage of stateful data. In keeping with the share nothing philosophy embraced by Rails, the proper place for persistent storage of data is the database, period. The bottom line is that the longer you keep objects in the user’s session hash, the more problems you create for yourself in trying to keep those objects from becoming stale (in other words, out of date in relation to the database).
This chapter deals with matters related to session use, starting with the question of what to put in the session.
13.1 What to Store in the Session
Deciding what to store in the session hash does not have to be super-difficult, if you simply commit to storing as little as possible in it. Generally speaking, integers (for key values) and short string messages are okay. Objects are not.
13.1.1 The Current User
There is one important integer that most Rails applications store in the session, and that is the current_user_id. Not the current user object, but its id. Even if you roll your own login and authentication code (which you shouldn’t do), don’t store the entire User (or Person) in the session while the user is logged in. (See Chapter 14, “Authentication and Authorization” for more information about keeping track of the current user.) The authentication system should take care of loading the user instance from the database prior to each request and making it available in a consistent fashion, via a method on your ApplicationController. In particular, following this advice will ensure that you are able to disable access to given users without having to wait for their session to expire.
13.1.2 Session Use Guidelines
Here are some more general guidelines on storing objects in the session:
· They must be serializable by Ruby’s Marshal API, which excludes certain types of objects such as a database connection and other types of I/O objects.
· Large object graphs may exceed the size available for session storage. Whether this limitation is in effect for you depends on the session store chosen and is covered later in the chapter.
· Critical data should not be stored in the session, since it can be suddenly lost by the user ending his session (by closing the browser or clearing his cookies).
· Objects with attributes that change often should not be kept in the session.
· Modifying the structure of an object and keeping old versions of it stored in the session is a recipe for disaster. Deployment scripts should clear old sessions to prevent this sort of problem from occurring, but with certain types of session stores, such as the cookie store, this problem is hard to mitigate. The simple answer (again) is to just not keep anything except for the occasional id in the session.
13.2 Session Options
You used to be able to turn off the session, but since Rails 2.3, applications that don’t need sessions don’t have to worry about them. Sessions are lazy-loaded, which means unless you access the session in a controller action, there is no performance implication.
13.3 Storage Mechanisms
The mechanism via which sessions are persisted can vary. Rails’ default behavior is to store session data as cookies in the browser, which is fine for almost all applications. If you need to exceed the 4KB storage limit inherent in using cookies, then you can opt for an alternative session store. But of course, you shouldn’t be exceeding that limit, because you shouldn’t be keeping much other than an id or two in the session.
There are also some potential security concerns around session-replay attacks involving cookies, which might push you in the direction of using an alternative session storage.
13.3.1 Active Record Session Store
In previous version of Rails, the ability to switch over to storing sessions in the database was built into framework itself. However as of version 4.0, the Active Record session store has been extracted into its own gem.
To get started using the Active Record session store, add the activerecord-session_store gem to your Gemfile and run bundle:
# Gemfile
gem 'activerecord-session_store'
The next step is to create the necessary migration, using a generator provided by the gem for that very purpose, and run the migration to create the new table:
$ rails generate active_record:session_migration
create db/migrate/20130821195235_add_sessions_table.rb
$ rake db:migrate
== AddSessionsTable: migrating ============================================
-- create_table(:sessions)
-> 0.0095s
-- add_index(:sessions, :session_id)
-> 0.0004s
-- add_index(:sessions, :updated_at)
-> 0.0004s
== AddSessionsTable: migrated (0.0104s)====================================
The final step is to tell Rails to use the new sessions table to store sessions, via a setting in config/initializers/session_store.rb:
Rails.application.config.session_store :active_record_store
That’s all there is to it.
Kevin says… The biggest problem with using the Active Record session store is that it adds an unnecessary load on your database. Each time a user reads or writes from the session, the database will be hit. |
13.3.2 Memcached Session Storage
If you are running an extremely high-traffic Rails deployment, you’re probably already leveraging Memcached in some way or another. The memcached server daemon is a remote-process memory cache that helps power some of the most highly trafficked sites on the Internet.
The memcached session storage option lets you use your memcached server as the repository for session data and is blazing fast. It’s also nice because it has built-in expiration, meaning you don’t have to expire old sessions yourself.
To use memcached, the first step is to add the dalli gem to your Gemfile and run bundle:
# Gemfile
gem 'dalli'
Next, setup your Rails environment to use memcached as its cache store. At a minimum, one can set the configuration setting cache_store to :mem_cache_store:
# config/environments/production.rb
config.cache_store = :mem_cache_store
Note In Rails 4, when defining a cache_store using option :mem_cache_store, the dalli72 gem is used behind the scenes instead of the memcache-client gem. Besides being threadsafe, which is Rails 4 is by default, here are some of the reasons why Dalli is the new default memcached client: · It is approximately 20% faster than the memcache-client gem. · Dalli has the ability to handle failover with recovery and adjustable timeouts. · Dalli uses the newer memcached binary protocol. For more details, see the Cache Storage section in the Caching and Performance chapter. |
Next, modify Rails’ default session store setting in config/initializers/session_store.rb. At minimum, replace the contents of the file with the following:
Rails.application.config.
session_store ActionDispatch::Session::CacheStore
This will tell Rails to use the cache_store of the application as the underlying session store as well. Additionally, one could explicitly set the amount of seconds a session is available for by setting the :expire_after option.
Rails.application.config.
session_store ActionDispatch::Session::CacheStore,
expires_after: 20.minutes
13.3.3 The Controversial CookieStore
In February 2007, core-team member Jeremy Kemper made a pretty bold commit to Rails. He changed the default session storage mechanism from the venerable PStore to a new system based on a CookieStore. His commit message summed it up well:
Introduce a cookie-based session store as the Rails default. Sessions typically contain at most a user_id and flash message; both fit within the 4K cookie size limit. A secure hash is included with the cookie to ensure data integrity (a user cannot alter his user_id without knowing the secret key included in the hash). If you have more than 4K of session data or don’t want your data to be visible to the user, pick another session store. Cookie-based sessions are dramatically faster than the alternatives.
I describe the CookieStore as controversial because of the fallout over making it the default session storage mechanism. For one, it imposes a very strict size limit, only 4KB. A significant size constraint like that is fine if you’re following the Rails way, and not storing anything other than integers and short strings in the session. If you’re bucking the guidelines, well, you might have an issue with it.
13.3.3.1 Encrypted Cookies
Lots of people have complained about the inherent insecurity of storing session information, including the current user information on the user’s browser. In Rails 3, cookies were only digitally signed, which verified that they were generated by your application and were difficult to alter. However, the contents of the cookie could still be easily read by the user. As of Rails 4, all cookies are encrypted by default, making them not only hard to alter, but hard to read too.
13.3.3.2 Replay Attacks
Another problem with cookie-based session storage is its vulnerability to replay attacks, which generated an enormous message thread on the rails-core mailing list. S. Robert James kicked off the thread73 by describing a replay attack:
Example:
1. User receives credits, stored in his session.
2. User buys something.
3. User gets his new, lower credits stored in his session.
4. Evil hacker takes his saved cookie from step 1 and pastes it back in his browser’s cookie jar. Now he’s gotten his credits back.
· This is normally solved using something called nonce. Each signing includes a once-only code, and the signer keeps track of all of the codes, and rejects any message with the code repeated. But that’s very hard to do here, since there may be several app servers serving up the same application.
· Of course, we could store the nonce in the DB, but that defeats the entire purpose!
The short answer is: Do not store sensitive data in the session. Ever. The longer answer is that coordination of nonces across multiple servers would require remote process interaction on a per-request basis, which negates the benefits of using the cookie session storage to begin with.
The cookie session storage also has potential issues with replay attacks that let malicious users on shared computers use stolen cookies to log in to an application that the user thought he had logged out of. The bottom line is that if you decide to use the cookie session storage on an application with security concerns, please consider the implications of doing so carefully.
13.3.4 Cleaning Up Old Sessions
If you’re using the activerecord-session_store gem, you can write your own little utilities for keeping the size of your session store under control. Listing 13.1 is a class that you can add to your /lib folder and invoke from the production console or a script whenever you need to do so.
Listing 13.1: SessionMaintenance class for cleaning up old sessions
1 classSessionMaintenance
2 defself.cleanup(period = 24.hours.ago)
3 session_store = ActiveRecord::SessionStore::Session
4 session_store.where('updated_at < ?', period).delete_all
5 end
6 end
13.4 Cookies
This section is about using cookies, not the cookie session store. The cookie container, as it’s known, looks like a hash, and is available via the cookies method in the scope of controllers. Lots of Rails developers use cookies to store user preferences and other small nonsensitive bits of data. Be careful not to store sensitive data in cookies, since they can be read by users. The cookies container is also available by default in view templates and helpers.
13.4.1 Reading and Writing Cookies
The cookie container is filled with cookies received along with the request, and sends out any cookies that you write to it with the response. Note that cookies are read by value, so you won’t get the cookie object itself back, just the value it holds as a string (or as an array of strings if it holds multiple values).
To create or update cookies, you simply assign values using the brackets operator. You may assign either a single string value or a hash containing options, such as :expires, which takes a number of seconds before which the cookie should be deleted by the browser. Remember that Rails convenience methods for time are useful here:
1 # writing a simple session cookie
2 cookies[:list_mode] = "false"
3
4 # specifying options, curly brackets are needed to avoid syntax error
5 cookies[:recheck] = { value: "false", expires: 5.minutes.from_now }
I find the :path options useful in allowing you to set options specific to particular sections or even particular records of your application. The :path option is set to '1', the root of your application, by default.
The :domain option allows you to specify a domain, which is most often used when you are serving up your application from a particular host, but want to set cookies for the whole domain.
1 cookies[:login] = {
2 value: @user.security_token,
3 domain: '.domain.com',
4 expires: Time.now.next_year
5 }
Cookies can also be written using the :secure option, and Rails will only ever transmit them over a secure HTTPS connection:
# writing a simple session cookie
cookies[:account_number] = { value: @account.number, secure: true }
The :httponly option tells Rails whether cookies can be accessible via scripting or only HTTP. It defaults to false.
Finally, you can delete cookies using the delete method:
cookies.delete :list_mode
13.4.1.1 Permanent Cookies
Writing cookies to the response via the cookies.permanent hash automatically gives them an expiration date 20 years in the future.
1 cookies.permanent[:remember_me] = current_user.id
13.4.1.2 Signed Cookies
Writing cookies to the response via the cookies.signed hash generates signed representations of cookies, to prevent tampering of that cookie’s value by the end user. If a signed cookie was tampered with a ActiveSupport::MessageVerifier::InvalidSignature exception will be raised when that cookie is read in a subsequent request.
cookies.signed[:remember_me] = current_user.id
13.5 Conclusion
Deciding how to use the session is one of the more challenging tasks that faces a web application developer. That’s why we put a couple of sections about it right in the beginning of this chapter. We also covered the various options available for configuring sessions, including storage mechanisms and methods for timing out sessions and the session lifecycle. We also covered use of a closely-related topic, browser cookies.