Add Purpose Metadata to Cookies#32937
Conversation
|
(@rails-bot has picked a reviewer for you, use r? to override) |
There was a problem hiding this comment.
We probably want the two new tests to set the ActionDispatch::Cookies::AbstractCookieJar.use_cookies_with_metadata config to true
There was a problem hiding this comment.
The tests related to purpose and expiry metadata are failing because I can't switch on - ActionDispatch::Cookies::AbstractCookieJar.use_cookies_with_metadata properly, which is set to false by default.
I tried setting ActionDispatch::Cookies::AbstractCookieJar.use_cookies_with_metadata config to true within the new tests, but don't know why it breaks a lot of other tests. Would it be because of the way the tests are run ?
I tried switching it to true in the metadata tests like this -
def test_purpose_metadata_for_encrypted_cookies
ActionDispatch::Cookies::AbstractCookieJar.use_cookies_with_metadata = true
cookies.encrypted[:user_id] = 50
cookies.encrypted[:discount_percentage] = 10
cookies[:discount_percentage] = cookies[:user_id]
assert_nil cookies.encrypted[:discount_percentage]
# ActionDispatch::Cookies::AbstractCookieJar.use_cookies_with_metadata = false
end
I also tried creating a subclass of CookiesTest with setup and teardown methods. But it screws up everything and other kinds of experimentation.
Could you suggest me the right way to do this? 😕
@Edouard-chin @kaspth @guilleiguaran
There was a problem hiding this comment.
It's probably because ActionDispatch::Cookies::AbstractCookieJar.use_cookies_with_metadata = false was never run since the execution stoped when the assertion assert_nil cookies.encrypted[:discount_percentage] failed.
There was a problem hiding this comment.
I had tried both. Are tests run parallelly ?
There was a problem hiding this comment.
They are not, I tried running the tests and they work fine with something like
Diff
diff --git a/actionpack/test/dispatch/cookies_test.rb b/actionpack/test/dispatch/cookies_test.rb
index aa9e4dac42..9a822c4f44 100644
--- a/actionpack/test/dispatch/cookies_test.rb
+++ b/actionpack/test/dispatch/cookies_test.rb
@@ -1301,19 +1301,27 @@ def test_vanilla_cookie_with_expires_set_relatively
end
def test_purpose_metadata_for_signed_cookies
+ previous_config = ActionDispatch::Cookies::AbstractCookieJar.use_cookies_with_metadata
+ ActionDispatch::Cookies::AbstractCookieJar.use_cookies_with_metadata = true
cookies.signed[:user_id] = 50
cookies.signed[:discount_percentage] = 10
cookies[:discount_percentage] = cookies[:user_id]
assert_nil cookies.signed[:discount_percentage]
+ ensure
+ ActionDispatch::Cookies::AbstractCookieJar.use_cookies_with_metadata = previous_config
end
def test_purpose_metadata_for_encrypted_cookies
+ previous_config = ActionDispatch::Cookies::AbstractCookieJar.use_cookies_with_metadata
+ ActionDispatch::Cookies::AbstractCookieJar.use_cookies_with_metadata = true
cookies.encrypted[:user_id] = 50
cookies.encrypted[:discount_percentage] = 10
cookies[:discount_percentage] = cookies[:user_id]
assert_nil cookies.encrypted[:discount_percentage]
+ ensure
+ ActionDispatch::Cookies::AbstractCookieJar.use_cookies_with_metadata = previous_config
endOnly two tests are failing related to expiry, but it makes sense since the expiry option is now set solely when use_cookies_with_metadata is true.
There was a problem hiding this comment.
Thanks, @Edouard-chin for helping me out with your expertise. Got to make a few more tweaks to the PR 😄
There was a problem hiding this comment.
Shouldn't this be assert_nil cookies.signed[:discount] instead?
There was a problem hiding this comment.
@Edouard-chin I've made some more changes 😄
There was a problem hiding this comment.
The tests related to purpose and expiry metadata are failing, because I can't switch on - ActionDispatch::Cookies::AbstractCookieJar.use_cookies_with_metadata which is set to false by default.
I tried turning it to true in the metadata tests like this -
def test_purpose_metadata_for_encrypted_cookies
ActionDispatch::Cookies::AbstractCookieJar.use_cookies_with_metadata = true
cookies.encrypted[:user_id] = 50
cookies.encrypted[:discount_percentage] = 10
cookies[:discount] = cookies[:user_id]
assert_nil cookies.encrypted[:discount_percentage]
# ActionDispatch::Cookies::AbstractCookieJar.use_cookies_with_metadata = false
end
I also tried creating a subclass of CookiesTest with setup and teardown methods. But it screws up everything and other kinds of experimentation.
Could you suggest me the right way to do this? 😕
1153258 to
11ae805
Compare
290e675 to
d898409
Compare
6819834 to
a8c88cd
Compare
mjc-gh
left a comment
There was a problem hiding this comment.
Looks great overall! Nice work!!
There was a problem hiding this comment.
Could make this a bit more concise with tap and a Hash literal:
{ purpose: options[:cookie_name] }.tap do |metadata|
# set expires_in or expires_at...
enda8c88cd to
803ab5c
Compare
There was a problem hiding this comment.
@mikeycgto Was this what you had suggested? 😄
There was a problem hiding this comment.
@mikeycgto @kaspth @guilleiguaran
Travis CI's GEM = aj:integration fails with a syntax error because of the output of <%= 1.month.from_now %>. Should I convert <%= 1.month.from_now %> into a string or any other format? Or am I missing anything?
aa4cd7e to
cf88f3b
Compare
There was a problem hiding this comment.
Maybe https://github.com/rails/rails/blob/master/railties/lib/rails/generators/rails/app/templates/config/initializers/new_framework_defaults_6_0.rb.tt is a better place for this?
@kaspth wdyt?
There was a problem hiding this comment.
@guilleiguaran Yes, that's the place we should put this.
kaspth
left a comment
There was a problem hiding this comment.
Initial pass! Looks like this mostly gets the job done, but the flow and readability isn't quite where we need it. But that's good! Now we have a base to expand on.
Left lots of questions to hopefully guide you in your further decision making.
There was a problem hiding this comment.
Having these ensures spread throughout is a bad practice.
Switch to:
with_cookie_metadata_enabled do
# …
endRemember to capture the old value in a local variable too and reassign it back.
There was a problem hiding this comment.
@guilleiguaran Yes, that's the place we should put this.
There was a problem hiding this comment.
Why did you make this a JSON value here? What's the benefit of this way, as opposed to deferring it? How does this choice mesh with our goal of programmer happiness and readability?
There was a problem hiding this comment.
Where is use_cookies_with_metadata set to true? Seems like we'd ship something that would never actually use metadata? And is it the best approach to split this over 2 options? E.g. is the mental overhead of 2 toggles worth it for end users?
There was a problem hiding this comment.
Put yourself in the shoes of a person who's upgrading their Rails app. What would they want to know here?
There was a problem hiding this comment.
Why is this named fetch? How are we fetching the metadata? And is it the input that's the most important thing here? Do we regularly call methods to pass something in or to get something out?
There was a problem hiding this comment.
Why is this method concerned with cookies_with_metadata?? Especially when there's a method that's called fetch_metadata? Same for the other instances.
There was a problem hiding this comment.
Won't this break old Rails apps as we're always writing the cookie_name to the cookie? E.g. would a cookie from 5.2 (e.g. no metadata) be readable on 6.0? And would a metadata attached cookie from 6.0 be readable on 5.2? This is one of the most important things we have to keep in mind, so do our tests test these scenarios sufficiently and do they do so in a sufficiently realistic manner?
There was a problem hiding this comment.
I still don't like assigning straight to the options here. They'll be committed in the cookie body after all. Have you tried passing the name in to commit or moving the metadata construction up to here?
There was a problem hiding this comment.
Calling @encryptor.decrypt_and_verify(encrypted_message, on_rotation: rotate) 3 times here is a smell.
There was a problem hiding this comment.
Think the tap here blurs the flow. Is building the metadata at the same level of abstraction as normalizing the expiry? If not, how can we compose this code better?
9e9aff9 to
490cfcb
Compare
|
@kaspth Here's the suggested changes 😄 |
There was a problem hiding this comment.
There's a typo here, I'll fix that...
sikachu
left a comment
There was a problem hiding this comment.
I do feel weird about having a hash ({ switch_on: true }) as a way to enable the feature. Is there a particular reason why we can't make falsy as an off state and any object as an on state?
i.e.
.cookies_with_purpose_metadata = false # off
.cookies_with_purpose_metadata = true # on, `honor_cookies_without_metadata_till` is nil
.cookies_with_purpose_metadata = { honor_cookies_without_metadata_till: 30.days.from_now }Also, I'm curious about the wording of honor_cookies_without_metadata_till. I think expanding till to until would make it reads better, and maybe we can change honor to allow to, just to make it more inline of other flags that available in Rails (assuming it still conveys the same message):
config.action_dispatch.cookies_with_purpose_metadata = {
allow_cookies_without_metadata_until: 30.days.from_now
}Please let me know what you think. Thank you for your patch.
There was a problem hiding this comment.
Do you mind adding some indentation for line 21 and 22 too?
490cfcb to
6d4fd58
Compare
ae121b9 to
5d03ffc
Compare
Purpose metadata prevents cookie values from being copy-pasted and ensures that the cookie is used only for its originally intended purpose. The Purpose and Expiry metadata are embedded inside signed/encrypted cookies and will not be readable on previous versions of Rails. We can switch off purpose and expiry metadata embedded in signed and encrypted cookies using config.action_dispatch.use_cookies_with_metadata = false if you want your cookies to be readable on older versions of Rails.
5d03ffc to
1cda4fb
Compare
kaspth
left a comment
There was a problem hiding this comment.
❤️👏
Let's clean up the test things and add a changelog in a separate PR. I don't want to hold this up any longer 😄
| assert_equal "5-2-Stable Chocolate Cookies", cookies.encrypted[:favorite] | ||
|
|
||
| freeze_time do | ||
| travel 1001.years |
There was a problem hiding this comment.
freeze_time just does travel_to Time.now, so you could just do travel 1001.years do.
…"Configuring Rails Applications" guide [ci skip] Related to rails#32937, rails#33605.
…[ci skip] Context rails#33605 (comment) Related to rails#32937, rails#33605
- There is a regression in 6.0 introduced by rails#32937 where cookie doesn't expire anymore unless the new `use_cookies_with_metadata` configuration is set to `true`. This causes issue for app migration from 5.2 to 6.0 because the `use_cookies_with_metadata` flag can't be set to true until all servers are running on 6.0. Here is a small reproduction script that you can run in the console ```ruby ActionDispatch::Cookies request = ActionDispatch::Request.empty request.env["action_dispatch.key_generator"] = ActiveSupport::KeyGenerator.new('1234567890') request.env["action_dispatch.signed_cookie_salt"] = 'signed cookie' request.env["action_dispatch.cookies_rotations"] = ActiveSupport::Messages::RotationConfiguration.new request.env["action_dispatch.use_authenticated_cookie_encryption"] = true signed_cookie = request.cookie_jar.signed signed_cookie[:foobar] = { value: '123', expires: 1.day.ago } p signed_cookie[:foobar] ```
Summary
This PR adds
purposemetadata to cookies so that the value of one cookie cannot be copied and used as the value of another cookie.Other Information
Turn on
config.action_dispatch.use_cookies_with_metadatato embed expiry and purpose metadata inside signed/encrypted cookies, and to verify if signed/encrypted cookie values have been copy-pasted. Previously set cookies (i.e. without purpose or expiry metadata) will continue to be honored.