Persistent cookies get downgraded to session cookies (key base rotation problem)

According to the docs Rails, (currently: version 6) supports two kinds of cookies: session cookies and persistent cookies; the latter having a defined expiry date at some time in the future (and might persist during a clientside reboot).

Sadly, the latter ones do not work as expected. Instead, they are silently changed to session cookies at the next encryption key rotation.

I might miss some point, but from studying the code, I get the impression that the whole thing, as promoted here, can never have worked. Or am I doing it wrong?

Some background:

Rails has a concept of a “rotation”. This means, one can change security parameters, and then do a phased upgrade, where the old and the new parameters are both valid for some time, and all encountered secrets will be transparently re-encrypted with the new parameters.

After such a rotation the cookies which are returned in from the clients will also need to be re-encrypted and then stored back onto the client. But the client does not return the expiration date of the cookie, so this is ‘nil’ when recreating the cookie, and consequentially the cookie becomes a session cookie. To solve this, one would need to store the expiration date explicitely within the cookie payload.

Addendum: I did some further research and found that we do already have the expiration date stored within the cookie payload!

This is done in ActiveSupport::Messages::Metadata. But that data is encapsulated there and only used to verify the validity of the cookie, it is not made accessible to the CookieJar. Grabbing it from there, moving it thru the MessageEncryptor and then onwards to the EncryptedRotatingKeyCookieJar and to the SerializedCookieJars (where we would need it) appears to be not something that is supposed to be done. (It appears to work, nevertheless.)

What appears strange to me: the Rotating Cookies were introduced somewhere at Rel. 5.2, and the Messages Metadata also appeared about 5.2 - but somehow it was not supposed to have these two connected.

Another word - why I care about this:

The secret key base is the core security anchor for the whole application. And it seems not popular to change it regularly. I don’t understand that: we have learned long ago that we must change our personal passwords every three months. We learn now that we should update certificates every three months. Security guys know that risks grow exponentially over time.

But with Rails, only a few years ago somebody had the great idea that it is not so good to store the secret key base on github. And then that was remedied. Well, somehow. It’s actually hard to believe.

The only one who seems to ever have cared about it is this guy here. And that works, and it can be automated.

Any ideas why this is not popular? I would rather think it should be the default to change the thing every three months, unless really serious reasons prohibit that.

… just here to share my delight: this seems to be fixed in Rails-8 ! :slight_smile:

Hey Peter,

Just curious, was this actually fixed in Rails 8? Do you have a link to the issue or PR? I’ve been trying to setup cookie rotation in my rails 8.0.2 app like so:

Rails.application.config.after_initialize do
  Rails.application.config.action_dispatch.cookies_rotations.tap do |cookies|
    old_secret_key_base = Rails.application.credentials.old_secret_key_base
    if old_secret_key_base.present?
      old_key_gen = ActiveSupport::KeyGenerator.new(old_secret_key_base, iterations: 1000)
      key_len = ActiveSupport::MessageEncryptor.key_len
      authenticated_encrypted_cookie_salt = Rails.application.config.action_dispatch.authenticated_encrypted_cookie_salt
      signed_cookie_salt = Rails.application.config.action_dispatch.signed_cookie_salt
      old_encrypted_secret = old_key_gen.generate_key(authenticated_encrypted_cookie_salt, key_len)
      old_signed_secret = old_key_gen.generate_key(signed_cookie_salt)

      cookies.rotate(:encrypted, old_encrypted_secret)
      cookies.rotate(:signed, old_signed_secret)
    end
  end
end

I noticed what you observed: that the persistent cookies all get converted to session cookies when rotated.

Thanks, Chris

Did you get an update on this? We are seeing the same thing.

What was the solution? That link no longer works.

Hi,

You’re right, it does not work in rails-8. It seems I had seen a date in there, and in wishful thinking assumed it would be the appropriate and required date, which it apparently isn’t. So it still needs an extremely ugly monkey-patch to grab that date out of ActiveSupport::Messages::Metadata and insert it into ActionDispatch::Cookies::SerializedCookieJars

And also, that way the date can be retrieved, but not the settings for same-site, httponly and secure.

The link for the original article seems to have changed to this one:

https://medium.com/vidio/rotating-ruby-on-rails-secret-key-base-9a4dcdf0d817

I could never get it to work and eventually gave up. If anyone figures out how to do it (even a monkey patch), please share in this thread. Thanks.

Since the cookie rotator verifies the cookie is valid, I wonder if I can then check if there is a session cookie, delete the old cookie and session cookie, and instead create a new cookie with the value.