.

Coffee Powered

code and content

Rails Cookie Sessions and PHP

I recently found myself needing to share session data from my Rails app with a PHP app on the same domain. We use cookie sessions for a number of reasons, and while they work great, the data stored in them is stored in Ruby’s native Marshal format, which is not trivial to reimplement in PHP. After trying to get the data unmarshaled for a bit, I had another idea – why not just change the storage format?

Fortunately, Ruby is deeply entangled with another more portable serialization format: YAML.

Rails manages its session cookies through the MessageVerifier. Easy enough – we can just write our own MessageVerifier that uses YAML rather than Marshal.


module ActiveSupport
  class YamlMessageVerifier < MessageVerifier
    def verify(signed_message)
      raise InvalidSignature if signed_message.blank?

      data, digest = signed_message.split("--")
      if data.present? && digest.present? && secure_compare(digest, generate_digest(data))
        str = ActiveSupport::Base64.decode64(data)
        if str[0..2] == '---'
          YAML::load str
        else # Handle old Marshal.dump'd session
          Marshal.load(str)
        end
      else
        raise InvalidSignature
      end
    end

    def generate(value)
      data = ActiveSupport::Base64.encode64s(YAML::dump value)
      "#{data}--#{generate_digest(data)}"
    end
  end
end

You’ll notice that verify() can accept a Marshaled session as well; this lets you transparently transition existing cookies to the new format without any kind of session breakage. Nice.

Now, to use the verifier, we monkeypatch CookieStore:

module ActionController
  module Session
    class CookieStore
      def verifier_for(secret, digest)
        key = secret.respond_to?(:call) ? secret.call : secret
        ActiveSupport::YamlMessageVerifier.new(key, digest)
      end
    end
  end
end

Now, this will work…at least at first glance, until you try to use the flash. This is a particularly nasty little problem, and it stems from the fact that Ruby’s YAML implementation serializes Hash objects without their instance variables, and FlashHash inherits from Hash, and thus inherits its serialization/deserialization strategy. I worked for a while to monkeypatch those strategies, but I didn’t like the result, and it felt a little hacky. Instead, I just took advantage of the YAML load lifecycle to make sure the FlashHash initializes properly:

module ActionController
  module Flash
    class FlashHash
      def update_with_initializer(h)
        @used ||= {}
        update_without_initializer(h)
      end
      alias_method_chain :update, :initializer
    end
  end
end

The core problem is that YAML::load calls Hash#update, and FlashHash presumes that the @used instance variable is present and initialized to an empty hash. To fix that, I just aliased in an initializer to make sure that variable is set.

Note that if you are storing other Hash subclasses with instance variables that rely on those variables being persisted across sessions, they will break. However, you should only be storing primitive/array/hash data in the session if possible. FlashHash is sort of a nasty violation of this principle.

At this point, your session should be serializing to and from YAML. We’ll want to read it from PHP, naturally. I’m using SPYC in the PHP project, which gets us Close Enough(TM). It doesn’t handle symbol keys, but we’ll handle those in the PHP itself.

Reading from PHP

Reading the data back out is surprisingly simple. We have to verify the authenticity of the data, of course, by checking the hash, but then you just base64 decode the data, load it with spyc, and perform some simple transformation to turn symbols into strings. If you wanted to make it even easier, you could monkeypatch the cookie store to call #stringify_keys! on your session hash before serializing it (and then call #with_indifferent_access on the hash when you deserialize it. Be aware of the speed impact of such a decision before you do it.)

function explode_symbols($arr) {
  $result = array();
  foreach($arr as $key => $val) {
    if(is_numeric($key) && $val[0] == ":") {
      $bits = explode(":", $val, 3);
      $result[trim($bits[1])] = trim($bits[2]);
    } elseif (is_array($val)) {
      $result[$key] = explode_symbols($val);
    } else {
      $result[$key] = $val;
    }
  }
  return $result;
}

function deserialize_session($session_key, $secret) {
  list($session64, $hash) = explode("--", $_COOKIE[$session_key], 2);
  if(hash_hmac("SHA1", $session64, $secret) == $hash) {
    $session = base64_decode($session64);
    return explode_symbols(spyc_load($session));
  } else {
    throw new Exception("Invalid session signature");
  }
}

$rails_session = deserialize_session("your_session_cookie_name", $your_session_cookie_secret);

Caveats

  • Be aware that YAML is slower than Marshal
  • Be aware that storing Hash subclasses in the session is likely going to Not Work.

And that’s all there is to it. You can now share data between the two apps via the session cookie.

  • Rodrigo

    Hey man love your article, can you share any git repo of this example?

  • http://coffeepowered.net Chris Heald

    Unfortunately, the repo I have this implemented in is private/proprietary.

  • Artimuz Pro

    Hi,
    Interesting post. However, this don’t work with rails 3, as a lot of refactoring has been done. I’m currently trying to reproduce the same result in rails 3.1, I’ll let you know my results.

  • http://coffeepowered.net Chris Heald

    Yeah, it’s definitely keyed to the Rails 2.3 cookie handler. However, the concepts should still apply to the Rails 3 handler. Definitely interested in your results!

  • Artimuz Pro

    So, here my results for Rails 3.1.1 :

      1. For YamlMessageVerifier, I don’t change anything. However, I noticed that the original MessageVerifier can be called with :serializer => YAML instead of using a specific class. For those who do not need to do the transition from the Marshal format, it is preferable.

      2. Next for CookieStore : the interresting section code for us has been moved to ActionDispatch, so :

        module ActionDispatch
          class Cookies
            class SignedCookieJar
              def initialize(parent_jar, secret)
                ensure_secret_secure(secret)
                @parent_jar = parent_jar
                @verifier   = ActiveSupport::YamlMessageVerifier.new(secret)          # or MessageVerifier.new(secret,:serializer=>YAML)
              end
            end
          end
        end

    3. OK for FlashHash. However, I draw your attention to the fact that if you reserialize your cookie in PHP, you also need to reconvert your :flash hash into a FlashHash (I’ve not done the job yet). That can be done in ruby side or when reserializing the cookie, you can specify the type like this :

        flash: !ruby/object:ActionDispatch::Flash::FlashHash
      closed: …

    4. One last thing : I experienced problems with Spyc for the deserialization part. Indeed, I have many nested arrays in my cookies (for example my Devise/Warden auth object) and Spyc doesn’t not handle it well (arrays are flattened). If you have admin access to your server I would recommend PECL Yaml instead (it works very well and much faster, because it’s not in PHP).

    I think that’s all !

  • Artimuz Pro

    Oh, I forgot one point :

      5. In rails 3, /lib classes are not loaded automatically. So I suggest you to require the 3 files at the top of Application class (config/application.rb).  I tried to add /lib in autoload_paths, but that don’t work well (classes in autoload are not loaded before use, so that don’t work for a mixin…)

  • Howard

    I recently came up against a similar problem, where I needed a Rails 3 app to share session storage with a legacy PHP app.

    http://watsonbox.github.com/blog/2012/05/01/sharing-session-between-rails-and-php/