Fork me on GitHub
26 Feb 2011

Conflict of session cookies with different domains in Rails 3

Imagine the situation - you’ve got a site, e.g., www.awesomestartup.com. You added authorization to it, so people can register and post their very important data to it. And of course they can select ‘keep me signed in’ check-box during their sign-in procedure.

Later, you decided to create some additional project, e.g. profit.awesomestartup.com. But it will be especially cool, if you can keep users, which were logged in on www.awesomestartup.com, logged in on profit.awesomestartup.com too. For that, you decided to move domain of cookies from the default one (www.awesomestartup.com) to .awesomestartup.com.

But then, some of your users started to complain they can’t log in to your site. I.e. they fills the log in form, clicks ‘Sign in’, but logging in is not successful. And you can’t repeat that bug, because it works fine on your computers. Clearly, the problem is related to cookies.

Why does it happen?

After changing the domain of the session’s cookie in the Rails settings, your app will set session’s cookie to domain .awesomestartup.com, but you still have the old session’s cookie on the www.awesomestartup.com domain too. So, a browser has 2 cookies, on the domain .awesomestartup.com and www.awesomestartup.com. Let’s name that cookie as ‘_session_cookie’. After submitting the log in form, browser will check what cookies match to www.awesomestartup.com and send the cookies to the server in the HTTP header by this way:

Cookie: _session_cookie=blabla; foo=bar; _session_cookie=another_blabla; bla=bla

As you see, there is no chance to understand what cookie is correct. And Rack just takes the first one. Take a look at the code at lib/rack/request.rb of Rack gem:

# According to RFC 2109:
#   If multiple cookies satisfy the criteria above, they are ordered in
#   the Cookie header such that those with more specific Path attributes
#   precede those with less specific.  Ordering with respect to other
#   attributes (e.g., Domain) is unspecified.
@env["rack.request.cookie_hash"] =
  Utils.parse_query(@env["rack.request.cookie_string"], ';,').inject({}) {|h,(k,v)|
    h[k] = Array === v ? v.first : v
    h
  }

So, Rack uses incorrect version of the session cookie. Let’s imagine that during handling of the request you set some session value:

session[:some_key] = 'some_value'

That value will be stored in the session cookie, and a browser will get a response with such header:

Set-Cookie: _session_cookie=changed_blabla; domain=.awesomestartup.com; path=/; HttpOnly

And browser will update the cookie ‘_session_cookie’ for domain .awesomestarup.com. But with the next request, it will send both cookies again:

Cookie: _session_cookie=blabla; foo=bar; _session_cookie=changed_blabla; bla=bla

As you see, it sends the changed second ‘_session_cookie’ cookie. But Rack will use the first one again, and that means you will miss all our changes made during the latest request.

That’s why your users can’t log in anymore - their session data isn’t stored more than for one request.

How to solve the issue - a simple way

The most simple solution there is just to rename the session cookie. Set new name in config/initializers/session_store.rb

AstrologySite::Application.config.session_store :cookie_store,
  :key => '_new_session_cookie',
  :domain => ".awesomestartup.com"

But by that way you will lose all data saved in sessions of your users.

How to solve the issue - a slightly more complex way

If you want to keep user’s data, you need some another way. Because you know that we used a default domain before, then you just need to explicitly remove session cookie on that old domain. But you can’t do something like this:

cookies.delete("_session_cookie", :domain => "www.awesomestartup.com")

because Rails will set sessioncookie to its new session value after the end of the request handling, and when it sets new value to the cookie, it removes the cookie from the list of deleted cookies (see the actionpack gem, lib/action_dispatch/middleware/cookies.rb):

def []=(key, options)
  ...

  @set_cookies[key] = options
  @delete_cookies.delete(key)
  value
end

So, you have to remove that cookie after setting the new value to the session cookie. And middlewares go to the rescue again! :)

Let’s add such Middleware (e.g. to lib/middlewares/clear_duplicated_session.rb):

module Middlewares
  class ClearDuplicatedSession

    def initialize(app)
      @app = app
    end

    def call(env)
      result = @app.call(env)

      if there_are_more_than_one_session_key_in_cookies?(env)
        delete_session_cookie_for_current_domain(env)
      end

      result
    end


    private

      def there_are_more_than_one_session_key_in_cookies?(env)
        entries = 0
        offset = 0
        while offset = env["HTTP_COOKIE"].to_s.index(get_session_key(env), offset)
          entries += 1
          offset += 1
        end
        entries > 1
      end


      # Sets expiration date = 1970-01-01 to the cookie, this way browser will
      # note the cookie is expired and will delete it
      def delete_session_cookie_for_current_domain(env)
        ::Rack::Utils.set_cookie_header!(
          env['action_controller.instance'].response.header, # contains response headers
          get_session_key(env), # gets the cookie session name, '_session_cookie' - for this example
          { :value => '', :path => '/', :expires => Time.at(0) }
        )
      end


      def get_session_key(env)
        env['rack.session'].instance_variable_get("@by").instance_variable_get("@key")
      end

  end
end

And add it to config/application.rb, before ActionDispatch::Cookies:

config.middleware.insert_before "ActionDispatch::Cookies", "Middlewares::ClearDuplicatedSession"

Now, every time it finds two session cookies in the request cookies line, it will explicitly set ‘Set-Cookie’ header to the response which will delete the session cookie with the default domain.

19 Dec 2010

Use Nokogiri in Rails 2.3.* Views for building XML

We used to use Builder for building XML Rails 2.3.* views. How we do that: we create e.g. app/views/posts/rss.xml.builder file and put our XML code to there using Builder::XmlMarkup syntax, which is described thoroughly in Rails API docs.

But there is one thing annoys me so much - handling of CDATA. It is generally a great idea to put CDATA around all content stored in XML which may contain any other tags, e.g. HTML content. So, if we suspect there is HTML content in some tag, we will wrap it to CDATA by the following way:

xml.some_field do
  xml.cdata!("value")
end

and it will generate such XML:

<some_field>
  <![CDATA[value]]>
</some_field>

Note the linebreak after the <some_field> tag - it is not cool it is there, because it will add additional whitespaces and linebreaks. Look:

doc = Nokogiri.XML("<some_field>
  <![CDATA[value]]>
</some_field>")
# => [#<Nokogiri::XML::Element:0x816181c8 ... ] 
doc.xpath("//some_field").text
# => "\n  value\n"

Fortunately, Nokogiri’s Builder handles CDATA more correctly:

Nokogiri::XML::Builder.new do |xml|
  xml.some_field do
    xml.cdata("value")
  end
end.to_xml

will generate XML:

<some_field><![CDATA[value]]></some_field>

So, we just need to use it in Rails views by some way. Actually, it is pretty simple. Template handlers are stored in the actionpack gem, by the path: lib/action_view/template_handlers/*. If you take a look at them, you will see they have quite simple structure - they all override just one method - TemplateHandler#compile. So, to create our own Nokogiri template handler, let’s add such code to the config/initializers dir:

# To avoid initializing it several times (can cause errors)
unless ActionView::TemplateHandlers.const_defined?("Nokogiri")
  require 'nokogiri'

  module ActionView
    module TemplateHandlers
      class Nokogiri < TemplateHandler
        include Compilable

        def compile template
          "_set_controller_content_type(Mime::XML);" +
          "builder = ::Nokogiri::XML::Builder.new do |xml|;" +
          "  #{template.source%};" +
          "end;" +
          "builder.to_xml"
        end
      end
    end
  end

  # To register the *.nokogiri files for handling by the 
  # current template handler
  ActionView::Template.register_template_handler :nokogiri, 
    ActionView::TemplateHandlers::Nokogiri
end

Now, create a file e.g. app/views/posts/rss.xml.nokogiri and put something like this to it:

xml.some_field do
  xml.cdata("value")
end

and it will generate XML:

<some_field><![CDATA[value]]></some_field>

Just as we need it.

P.S.: Of course you can say: “Why just don’t strip whitespaces on the consumer’s side?” Sure you can, but in my case I don’t have access to third party consumers, and I can’t implement such functionality there. Moreover, Nokogiri now is a “de-facto” tool for all XML-related stuff in Ruby, so why don’t use it everywhere? :)