Iframe + Polling = Cookie Fight!

Background

Our application’s UI consists of 2 types of screens: the usual web pages and the game screen. We wanted our users to get to the game screen as quickly as possible, because that’s where all the fun happens. So when you get to the homepage, you click Play and you’re in. You don’t need to sign up or deposit at this point, you can do that on the game screen after you have seen how fun the game is.

The home page and the game screen

We’ve already had a working signup, login, ID verification, depositing pages implemented, so a logical solution was to pop up an iframe with the existing signup page and a minimal layout, so the user doesn’t have to leave the game.

The popup on the game screen

Our game client uses ajax polling, i.e. makes a request to the server about every second. We decided to do this, so we can release our new game sooner, and it will be good enough under the current load with a little help from memcached.

The problem

If every request would take 0 seconds from the browser’s point of view, we would have had no problems. However, because we store the session in a cookie, when an Ajax request and session-changing request in the iframe overlap, there was a major cookie-fight between:

Graph of the cookie-fight

  1. Iframe sends a request with Cookie A, which is a signup
  2. Ajax sends a request with Cookie A
  3. Iframe gets the response with Set-Cookie B, which contains the information that the user is logged in; the browser sets the cookie to B
  4. Ajax receives a response with Cookie A, because the backend (Rails app) always sends Set-Cookie header when the session was accessed, and it doesn’t know about B, because that’s not stored anywhere on the backend. So the browser set the cookie to A.
  5. When the user goes to the next step, e.g. depositing, the Iframe will send Cookie A, which tells the backend that the user is not logged in, so we ask the user to log in.

Our fix

The first fix was to avoid accessing the session variable in Rails for requests that don’t need authentication. I wrote a SessionDisabler mixin for controllers, and invoked it as filters:

# in the superclass of all API controllers:
prepend_before_filter :disable_session

# some controllers that need authentication:
skip_before_filter :disable_session, :only => :create

The problem has apparently gone away, but a user reported a problem where he was repeatedly logged out automatically. This must have been caused by the fact that requests that still need authentication keep resetting the session cookie. Actually, even if these requests need authentication, they don’t need to set the cookie, none of them is doing login or logout.

So all we had to do is to remove this Set-Cookie header from the responses for all API requests.

There is a directive in Nginx’s HttpProxyModule, proxy_hide_header, but Nginx was unhelpful, as it only matches a single location directive for a request.

The last thing we want to do is to duplicate everything in the location / section for location /api/v1/.

Luckily, we use chef, which can generate the config file from an erb template:

# vhost.conf.erb

  …
  <% ['/', '/api/v1/'].each do |path| -%>
  location <%= path %> {
    …
    <% if path == '/api/v1/' -%>
    proxy_hide_header Set-Cookie;
    <% end -%>
    …
  }
  <% end -%>
  …

This way, the duplication will only exist in the generated config file.

Posted on 23 August 2011 by Levente @leente.
blog comments powered by Disqus