Search code examples
ruby-on-railssessioncookiesamazon-elb

Why is rails constantly sending back a Set-Cookie header?


I'm experiencing issues with an elastic load balancer and varnish cache with respect to cookies and sessions getting mixed up between rails and the client. Part of the problem is, rails is adding a "Set-Cookie" header on with a session id on almost every request. If the client already is sending session_id, and it matches the session_id that rails is going to set.. why would rails continuously tell clients "oh yeah.. you're session id is ..."


Solution

  • Summary: Set-Cookie headers are set on almost every response, because

    1. the default session store will try to write the session data to an encrypted cookie on any request that has accessed the session (either to read from it or write to it),
    2. the encrypted value changes even when the plain text value hasn't,
    3. the encryption happens before it reaches the code that's responsible for checking if a cookie value has changed to avoid redundant Set-Cookie headers.

    Plain-text cookies

    In Rails, the ActionDispatch::Cookies middleware is responsible for writing Set-Cookie response headers based on the contents of a ActionDispatch::Cookies::CookieJar.

    The normal behaviour is what you'd expect: if a cookie's value hasn't changed from what was in the request's Cookie header, and the expiry date isn't being updated, then Rails won't send a new Set-Cookie header in the response.

    This is taken care of by a conditional in CookieJar#[]= which compares the value already stored in the cookie jar against the new value that's being written.

    Encrypted cookies

    To handle encrypted cookies, Rails provides an ActionDispatch::Cookies::EncryptedCookieJar class.

    The EncryptedCookieJar relies on ActiveSupport::MessageEncryptor to provide the encryption and decryption, which uses a random initialisation vector every time it's called. This means it's almost guaranteed to return a different encrypted string even when it's given the same plain text string. In other words, if I decrypt my session data, and then re-encrypt it, I'll end up with a different string to the one I started with.

    The EncryptedCookieJar doesn't do very much: it wraps a regular CookieJar, and just provides encryption as data goes in, and decryption as data comes back out. This means that the CookieJar#[]= method is still responsible for checking if a cookie's value has changed, and it doesn't even know the value it's been given is encrypted.

    These two properties of the EncryptedCookieJar explain why setting an encrypted cookie without changing its value will always result in a Set-Cookie header.

    The session store

    Rails provides different session stores. Most of them store the session data on a server (e.g. in memcached), but the default— ActionDispatch::Session::CookieStore—uses EncryptedCookieJar to store all of the data in an encrypted cookie.

    ActionDispatch::Session::CookieStore inherits a #commit_session? method from Rack::Session::Abstract::Persisted, which determines if the cookie should be set. If the session's been loaded, then the answer is pretty much always “yes, set the cookie”.

    As we've already seen, in the cases where the session's been loaded but not changed we're still going to end up with a different encrypted value, and therefore a Set-Cookie header.