We build Web & Mobile Applications.
Here’s a revisited post that’s fairly short and sweet: way back in 2008 I blogged about my implementation of tamper-proof cookies which used a similar technique to that used by Rails for its cookie-based session store. Back then the solution involved a custom cookie jar, the OpenSSL library to generate a HMAC, overriding the ApplicationController#cookies
method and a slightly unorthodox method signature for reading cookie values.
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:
nil
for the value whereas my code raised a TamperedWithCookie
exception.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.
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
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.