Here’s a recap of the code in action:

# Write a tamper-proof cookie
cookies[:test_protected] = { :value => "protected", :protect_from_forgery => true }

# Read a tamper-proof cookie
cookies[:test_protected, true]
=> "protected"

# Read a raw cookie
# This returns the actual contents of the cookie, including the HMAC, and does not perform tamper checking
cookies[:test_protected]
=> "protected--ecec30eed6e122f3a3d5bb914bdd3cc1da4bd28e"

And here’s the nicer Rails 3 version:

# Write a tamper-proof cookie
cookies.signed[:test_protected] = "protected"

# Read a tamper-proof cookie
cookies.signed[:test_protected]
=> "protected"

# Read the raw cookie (this gives you the encoded value and the HMAC)
cookies[:test_protected]
=> "BAhJIg5wcm90ZWN0ZWQGOgZFRg==--18368dd442679a298bc98267d1d45c3046f636a7"

As you can see the magic happens when you use the signed method to read and write cookie values. There are two differences between my code and the Rails 3 version:

  • Rails 3 automatically Base64 encodes the cookie value and the HMAC is generated using the encoded rather than the raw value.
  • When a tampered with cookie is detected Rails 3 will return nil for the value whereas my code raised a TamperedWithCookie exception.

Configuration

The only configuration option you might need to adjust is the secret token used when generating the HMAC. Rails will automatically generate a long, random string when you generate a new app that looks something like this:

# lives in config/initializers/secret_token.rb
MyApp::Application.config.secret_token = 'bdf998dad6dcc939bb3285131f2c902ec6d586ae973c02b05ad5ad2be47ade55054be99717a7c6e4191c3283fe6cedf7555126d9d360b996134f5978bc9520c8'

Normally you’ll not need to change this, but if you do make sure it is sufficiently long (minimum of 30 characters) and random enough so that it can’t be guessed easily, otherwise your tamper-proof cookies become somewhat less useful.

Rails 2.3.6 and newer can do it to

Despite the misleading title of this blog it’s actually not just Rails 3 that has this neat functionality: it first appeared in Rails 2.3.6 last May. Usage is exactly the same, but you will need to set the cookie_verifier_secret setting in an initializer like this:

ActionController::Base.cookie_verifier_secret = 'bdf998dad6dcc939bb3285131f2c902ec6d586ae973c02b05ad5ad2be47ade55054be99717a7c6e4191c3283fe6cedf7555126d9d360b996134f5978bc9520c8'

Remember to set your own secret (don’t just copy this one!), Rails has a handy rake task to help you:

rake secret
=> 54540ac4c22d48e24a88398d360b52465e2457a5d6bee99dc897cf5362098555b0940734a1a58b576c2138eea444be392412e81fb712d961a622b099e0cdae74

Rails 2.3.0 to 2.3.5 with ActiveSupport MessageVerifier

If you’re a fan of the classics you may still be using a version of Rails from 2.3.0 to 2.3.5. Under the hood newer Rails versions are using the ActiveSupport::MessageVerifier class to sign and verify cookies. The good news is that this class first made an appearance in Rails 2.3.0 so you can enjoy tamper-proof cookie goodness too. For example:

class ApplicationController < ActionController::Base
  protected
  
    def read_verified_cookie(name)
      cookie_verifier.verify(cookies[name])
    rescue ActiveSupport::MessageVerifier::InvalidSignature
      nil
    end

    def write_verified_cookie(name, value, options = {})
      cookies[name] = options.merge(:value => cookie_verifier.generate(value))
    end
    
    def cookie_verifier
      # Note: the 'secret' string should really live in a configuration file
      @cookie_verifier ||= ActiveSupport::MessageVerifier.new("54540ac4c22d48e24a88398d360b52465e2457a5d6bee99dc897cf5362098555b0940734a1a58b576c2138eea444be392412e81fb712d961a622b099e0cdae74")
    end
end

If you don’t mind the ActiveSupport dependency then you can also do a similar thing in a Sinatra application too.

And finally if your app is using a really old pre-2.3 version of Rails you might as well stick with my trusty old cookie jar from the original blog, although I’d recommend tweaking the code to remove the TamperedWithCookie exception and to Base64 encode the cookie values to make it easier if you migrate to Rails 3 in the future.