Skip to content

Commit 274a3aa

Browse files
committed
Allow transparent upgrading of legacy signed cookies to encrypted cookies; Automatically configure cookie-based sessions to use the best cookie jar given the app's config
1 parent 69c6513 commit 274a3aa

11 files changed

Lines changed: 239 additions & 99 deletions

File tree

‎actionpack/CHANGELOG.md‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,16 @@
11
## Rails 4.0.0 (unreleased) ##
22

3+
* Automatically configure cookie-based sessions to be encrypted if
4+
`secret_key_base` is set, falling back to signed if only `secret_token`
5+
is set. Automatically upgrade existing signed cookie-based sessions from
6+
Rails 3.x to be encrypted if both `secret_key_base` and `secret_token`
7+
are set, or signed with the new key generator if only `secret_token` is
8+
set. This leaves only the `config.session_store :cookie_store` option and
9+
removes the two new options introduced in 4.0.0.beta1:
10+
`encrypted_cookie_store` and `upgrade_signature_to_encryption_cookie_store`.
11+
12+
*Trevor Turk*
13+
314
* Ensure consistent fallback to the default layout lookup for layouts set
415
using symbols or procs that return nil.
516

‎actionpack/lib/action_dispatch.rb‎

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,8 +84,6 @@ module Http
8484
module Session
8585
autoload :AbstractStore, 'action_dispatch/middleware/session/abstract_store'
8686
autoload :CookieStore, 'action_dispatch/middleware/session/cookie_store'
87-
autoload :EncryptedCookieStore, 'action_dispatch/middleware/session/cookie_store'
88-
autoload :UpgradeSignatureToEncryptionCookieStore, 'action_dispatch/middleware/session/cookie_store'
8987
autoload :MemCacheStore, 'action_dispatch/middleware/session/mem_cache_store'
9088
autoload :CacheStore, 'action_dispatch/middleware/session/cache_store'
9189
end

‎actionpack/lib/action_dispatch/middleware/cookies.rb‎

Lines changed: 77 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,9 @@ def permanent
117117
# the cookie again. This is useful for creating cookies with values that the user is not supposed to change. If a signed
118118
# cookie was tampered with by the user (or a 3rd party), nil will be returned.
119119
#
120+
# If +config.secret_key_base+ and +config.secret_token+ (deprecated) are both set,
121+
# legacy cookies signed with the old key generator will be transparently upgraded.
122+
#
120123
# This jar requires that you set a suitable secret for the verification on your app's +config.secret_key_base+.
121124
#
122125
# Example:
@@ -126,23 +129,20 @@ def permanent
126129
#
127130
# cookies.signed[:discount] # => 45
128131
def signed
129-
@signed ||= begin
130-
if @options[:upgrade_legacy_signed_cookie_jar]
132+
@signed ||=
133+
if @options[:upgrade_legacy_signed_cookies]
131134
UpgradeLegacySignedCookieJar.new(self, @key_generator, @options)
132135
else
133136
SignedCookieJar.new(self, @key_generator, @options)
134137
end
135-
end
136-
end
137-
138-
# Only needed for supporting the +UpgradeSignatureToEncryptionCookieStore+, users and plugin authors should not use this
139-
def signed_using_old_secret #:nodoc:
140-
@signed_using_old_secret ||= SignedCookieJar.new(self, ActiveSupport::DummyKeyGenerator.new(@options[:secret_token]), @options)
141138
end
142139

143140
# Returns a jar that'll automatically encrypt cookie values before sending them to the client and will decrypt them for read.
144141
# If the cookie was tampered with by the user (or a 3rd party), nil will be returned.
145142
#
143+
# If +config.secret_key_base+ and +config.secret_token+ (deprecated) are both set,
144+
# legacy cookies signed with the old key generator will be transparently upgraded.
145+
#
146146
# This jar requires that you set a suitable secret for the verification on your app's +config.secret_key_base+.
147147
#
148148
# Example:
@@ -152,7 +152,38 @@ def signed_using_old_secret #:nodoc:
152152
#
153153
# cookies.encrypted[:discount] # => 45
154154
def encrypted
155-
@encrypted ||= EncryptedCookieJar.new(self, @key_generator, @options)
155+
@encrypted ||=
156+
if @options[:upgrade_legacy_signed_cookies]
157+
UpgradeLegacyEncryptedCookieJar.new(self, @key_generator, @options)
158+
else
159+
EncryptedCookieJar.new(self, @key_generator, @options)
160+
end
161+
end
162+
163+
# Returns the +signed+ or +encrypted jar, preferring +encrypted+ if +secret_key_base+ is set.
164+
# Used by ActionDispatch::Session::CookieStore to avoid the need to introduce new cookie stores.
165+
def signed_or_encrypted
166+
@signed_or_encrypted ||=
167+
if @options[:secret_key_base].present?
168+
encrypted
169+
else
170+
signed
171+
end
172+
end
173+
end
174+
175+
module VerifyAndUpgradeLegacySignedMessage
176+
def initialize(*args)
177+
super
178+
@legacy_verifier = ActiveSupport::MessageVerifier.new(@options[:secret_token])
179+
end
180+
181+
def verify_and_upgrade_legacy_signed_message(name, signed_message)
182+
@legacy_verifier.verify(signed_message).tap do |value|
183+
self[name] = value
184+
end
185+
rescue ActiveSupport::MessageVerifier::InvalidSignature
186+
nil
156187
end
157188
end
158189

@@ -179,7 +210,7 @@ def self.options_for_env(env) #:nodoc:
179210
encrypted_signed_cookie_salt: env[ENCRYPTED_SIGNED_COOKIE_SALT] || '',
180211
secret_token: env[SECRET_TOKEN],
181212
secret_key_base: env[SECRET_KEY_BASE],
182-
upgrade_legacy_signed_cookie_jar: env[SECRET_TOKEN].present? && env[SECRET_KEY_BASE].present?
213+
upgrade_legacy_signed_cookies: env[SECRET_TOKEN].present? && env[SECRET_KEY_BASE].present?
183214
}
184215
end
185216

@@ -354,10 +385,8 @@ def initialize(parent_jar, key_generator, options = {})
354385

355386
def [](name)
356387
if signed_message = @parent_jar[name]
357-
@verifier.verify(signed_message)
388+
verify(signed_message)
358389
end
359-
rescue ActiveSupport::MessageVerifier::InvalidSignature
360-
nil
361390
end
362391

363392
def []=(key, options)
@@ -371,46 +400,37 @@ def []=(key, options)
371400
raise CookieOverflow if options[:value].size > MAX_COOKIE_SIZE
372401
@parent_jar[key] = options
373402
end
403+
404+
private
405+
406+
def verify(signed_message)
407+
@verifier.verify(signed_message)
408+
rescue ActiveSupport::MessageVerifier::InvalidSignature
409+
nil
410+
end
374411
end
375412

376413
# UpgradeLegacySignedCookieJar is used instead of SignedCookieJar if
377414
# config.secret_token and config.secret_key_base are both set. It reads
378415
# legacy cookies signed with the old dummy key generator and re-saves
379416
# them using the new key generator to provide a smooth upgrade path.
380417
class UpgradeLegacySignedCookieJar < SignedCookieJar #:nodoc:
381-
def initialize(*args)
382-
super
383-
@legacy_verifier = ActiveSupport::MessageVerifier.new(@options[:secret_token])
384-
end
418+
include VerifyAndUpgradeLegacySignedMessage
385419

386420
def [](name)
387421
if signed_message = @parent_jar[name]
388-
verify_signed_message(signed_message) || verify_and_upgrade_legacy_signed_message(name, signed_message)
422+
verify(signed_message) || verify_and_upgrade_legacy_signed_message(name, signed_message)
389423
end
390424
end
391-
392-
def verify_signed_message(signed_message)
393-
@verifier.verify(signed_message)
394-
rescue ActiveSupport::MessageVerifier::InvalidSignature
395-
nil
396-
end
397-
398-
def verify_and_upgrade_legacy_signed_message(name, signed_message)
399-
@legacy_verifier.verify(signed_message).tap do |value|
400-
self[name] = value
401-
end
402-
rescue ActiveSupport::MessageVerifier::InvalidSignature
403-
nil
404-
end
405425
end
406426

407427
class EncryptedCookieJar #:nodoc:
408428
include ChainedCookieJars
409429

410430
def initialize(parent_jar, key_generator, options = {})
411431
if ActiveSupport::DummyKeyGenerator === key_generator
412-
raise "Encrypted Cookies must be used in conjunction with config.secret_key_base." +
413-
"Set config.secret_key_base in config/initializers/secret_token.rb"
432+
raise "You didn't set config.secret_key_base, which is required for this cookie jar. " +
433+
"Read the upgrade documentation to learn more about this new config option."
414434
end
415435

416436
@parent_jar = parent_jar
@@ -422,11 +442,8 @@ def initialize(parent_jar, key_generator, options = {})
422442

423443
def [](key)
424444
if encrypted_message = @parent_jar[key]
425-
@encryptor.decrypt_and_verify(encrypted_message)
445+
decrypt_and_verify(encrypted_message)
426446
end
427-
rescue ActiveSupport::MessageVerifier::InvalidSignature,
428-
ActiveSupport::MessageEncryptor::InvalidMessage
429-
nil
430447
end
431448

432449
def []=(key, options)
@@ -440,6 +457,28 @@ def []=(key, options)
440457
raise CookieOverflow if options[:value].size > MAX_COOKIE_SIZE
441458
@parent_jar[key] = options
442459
end
460+
461+
private
462+
463+
def decrypt_and_verify(encrypted_message)
464+
@encryptor.decrypt_and_verify(encrypted_message)
465+
rescue ActiveSupport::MessageVerifier::InvalidSignature, ActiveSupport::MessageEncryptor::InvalidMessage
466+
nil
467+
end
468+
end
469+
470+
# UpgradeLegacyEncryptedCookieJar is used by ActionDispatch::Session::CookieStore
471+
# instead of EncryptedCookieJar if config.secret_token and config.secret_key_base
472+
# are both set. It reads legacy cookies signed with the old dummy key generator and
473+
# encrypts and re-saves them using the new key generator to provide a smooth upgrade path.
474+
class UpgradeLegacyEncryptedCookieJar < EncryptedCookieJar #:nodoc:
475+
include VerifyAndUpgradeLegacySignedMessage
476+
477+
def [](name)
478+
if encrypted_or_signed_message = @parent_jar[name]
479+
decrypt_and_verify(encrypted_or_signed_message) || verify_and_upgrade_legacy_signed_message(name, encrypted_or_signed_message)
480+
end
481+
end
443482
end
444483

445484
def initialize(app)

‎actionpack/lib/action_dispatch/middleware/session/cookie_store.rb‎

Lines changed: 1 addition & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -100,42 +100,7 @@ def get_cookie(env)
100100

101101
def cookie_jar(env)
102102
request = ActionDispatch::Request.new(env)
103-
request.cookie_jar.signed
104-
end
105-
end
106-
107-
class EncryptedCookieStore < CookieStore
108-
109-
private
110-
111-
def cookie_jar(env)
112-
request = ActionDispatch::Request.new(env)
113-
request.cookie_jar.encrypted
114-
end
115-
end
116-
117-
# This cookie store helps you upgrading apps that use +CookieStore+ to the new default +EncryptedCookieStore+
118-
# To use this CookieStore set
119-
#
120-
# Myapp::Application.config.session_store :upgrade_signature_to_encryption_cookie_store, key: '_myapp_session'
121-
#
122-
# in your config/initializers/session_store.rb
123-
#
124-
# You will also need to add
125-
#
126-
# Myapp::Application.config.secret_key_base = 'some secret'
127-
#
128-
# in your config/initializers/secret_token.rb, but do not remove +Myapp::Application.config.secret_token = 'some secret'+
129-
class UpgradeSignatureToEncryptionCookieStore < EncryptedCookieStore
130-
private
131-
132-
def get_cookie(env)
133-
signed_using_old_secret_cookie_jar(env)[@key] || cookie_jar(env)[@key]
134-
end
135-
136-
def signed_using_old_secret_cookie_jar(env)
137-
request = ActionDispatch::Request.new(env)
138-
request.cookie_jar.signed_using_old_secret
103+
request.cookie_jar.signed_or_encrypted
139104
end
140105
end
141106
end

‎actionpack/test/dispatch/cookies_test.rb‎

Lines changed: 73 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,11 @@ def set_encrypted_cookie
8686
head :ok
8787
end
8888

89+
def get_encrypted_cookie
90+
cookies.encrypted[:foo]
91+
head :ok
92+
end
93+
8994
def set_invalid_encrypted_cookie
9095
cookies[:invalid_cookie] = 'invalid--9170e00a57cfc27083363b5c75b835e477bd90cf'
9196
head :ok
@@ -456,7 +461,42 @@ def test_signed_uses_upgrade_legacy_signed_cookie_jar_if_both_secret_token_and_s
456461
assert_kind_of ActionDispatch::Cookies::UpgradeLegacySignedCookieJar, cookies.signed
457462
end
458463

459-
def test_legacy_signed_cookie_is_read_and_transparently_upgraded_if_both_secret_token_and_secret_key_base_are_set
464+
def test_signed_or_encrypted_uses_signed_cookie_jar_if_only_secret_token_is_set
465+
@request.env["action_dispatch.secret_token"] = "b3c631c314c0bbca50c1b2843150fe33"
466+
@request.env["action_dispatch.secret_key_base"] = nil
467+
get :get_encrypted_cookie
468+
assert_kind_of ActionDispatch::Cookies::SignedCookieJar, cookies.signed_or_encrypted
469+
end
470+
471+
def test_signed_or_encrypted_uses_encrypted_cookie_jar_if_only_secret_key_base_is_set
472+
@request.env["action_dispatch.secret_token"] = nil
473+
@request.env["action_dispatch.secret_key_base"] = "c3b95688f35581fad38df788add315ff"
474+
get :get_encrypted_cookie
475+
assert_kind_of ActionDispatch::Cookies::EncryptedCookieJar, cookies.signed_or_encrypted
476+
end
477+
478+
def test_signed_or_encrypted_uses_upgrade_legacy_encrypted_cookie_jar_if_both_secret_token_and_secret_key_base_are_set
479+
@request.env["action_dispatch.secret_token"] = "b3c631c314c0bbca50c1b2843150fe33"
480+
@request.env["action_dispatch.secret_key_base"] = "c3b95688f35581fad38df788add315ff"
481+
get :get_encrypted_cookie
482+
assert_kind_of ActionDispatch::Cookies::UpgradeLegacyEncryptedCookieJar, cookies.signed_or_encrypted
483+
end
484+
485+
def test_encrypted_uses_encrypted_cookie_jar_if_only_secret_key_base_is_set
486+
@request.env["action_dispatch.secret_token"] = nil
487+
@request.env["action_dispatch.secret_key_base"] = "c3b95688f35581fad38df788add315ff"
488+
get :get_encrypted_cookie
489+
assert_kind_of ActionDispatch::Cookies::EncryptedCookieJar, cookies.encrypted
490+
end
491+
492+
def test_encrypted_uses_upgrade_legacy_encrypted_cookie_jar_if_both_secret_token_and_secret_key_base_are_set
493+
@request.env["action_dispatch.secret_token"] = "b3c631c314c0bbca50c1b2843150fe33"
494+
@request.env["action_dispatch.secret_key_base"] = "c3b95688f35581fad38df788add315ff"
495+
get :get_encrypted_cookie
496+
assert_kind_of ActionDispatch::Cookies::UpgradeLegacyEncryptedCookieJar, cookies.encrypted
497+
end
498+
499+
def test_legacy_signed_cookie_is_read_and_transparently_upgraded_by_signed_cookie_jar_if_both_secret_token_and_secret_key_base_are_set
460500
@request.env["action_dispatch.secret_token"] = "b3c631c314c0bbca50c1b2843150fe33"
461501
@request.env["action_dispatch.secret_key_base"] = "c3b95688f35581fad38df788add315ff"
462502

@@ -473,7 +513,27 @@ def test_legacy_signed_cookie_is_read_and_transparently_upgraded_if_both_secret_
473513
assert_equal 45, verifier.verify(@response.cookies["user_id"])
474514
end
475515

476-
def test_legacy_signed_cookie_is_nil_if_tampered
516+
def test_legacy_signed_cookie_is_read_and_transparently_encrypted_by_encrypted_cookie_jar_if_both_secret_token_and_secret_key_base_are_set
517+
@request.env["action_dispatch.secret_token"] = "b3c631c314c0bbca50c1b2843150fe33"
518+
@request.env["action_dispatch.secret_key_base"] = "c3b95688f35581fad38df788add315ff"
519+
@request.env["action_dispatch.encrypted_cookie_salt"] = "4433796b79d99a7735553e316522acee"
520+
@request.env["action_dispatch.encrypted_signed_cookie_salt"] = "00646eb40062e1b1deff205a27cd30f9"
521+
522+
legacy_value = ActiveSupport::MessageVerifier.new("b3c631c314c0bbca50c1b2843150fe33").generate('bar')
523+
524+
@request.headers["Cookie"] = "foo=#{legacy_value}"
525+
get :get_encrypted_cookie
526+
527+
assert_equal 'bar', @controller.send(:cookies).encrypted[:foo]
528+
529+
key_generator = @request.env["action_dispatch.key_generator"]
530+
secret = key_generator.generate_key(@request.env["action_dispatch.encrypted_cookie_salt"])
531+
sign_secret = key_generator.generate_key(@request.env["action_dispatch.encrypted_signed_cookie_salt"])
532+
encryptor = ActiveSupport::MessageEncryptor.new(secret, sign_secret)
533+
assert_equal 'bar', encryptor.decrypt_and_verify(@response.cookies["foo"])
534+
end
535+
536+
def test_legacy_signed_cookie_is_treated_as_nil_by_signed_cookie_jar_if_tampered
477537
@request.env["action_dispatch.secret_token"] = "b3c631c314c0bbca50c1b2843150fe33"
478538
@request.env["action_dispatch.secret_key_base"] = "c3b95688f35581fad38df788add315ff"
479539

@@ -484,6 +544,17 @@ def test_legacy_signed_cookie_is_nil_if_tampered
484544
assert_equal nil, @response.cookies["user_id"]
485545
end
486546

547+
def test_legacy_signed_cookie_is_treated_as_nil_by_encrypted_cookie_jar_if_tampered
548+
@request.env["action_dispatch.secret_token"] = "b3c631c314c0bbca50c1b2843150fe33"
549+
@request.env["action_dispatch.secret_key_base"] = "c3b95688f35581fad38df788add315ff"
550+
551+
@request.headers["Cookie"] = "foo=baz"
552+
get :get_encrypted_cookie
553+
554+
assert_equal nil, @controller.send(:cookies).encrypted[:foo]
555+
assert_equal nil, @response.cookies["foo"]
556+
end
557+
487558
def test_cookie_with_all_domain_option
488559
get :set_cookie_with_domain
489560
assert_response :success
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
# Be sure to restart your server when you modify this file.
22

3-
Blog::Application.config.session_store :encrypted_cookie_store, key: '_blog_session'
3+
Blog::Application.config.session_store :cookie_store, key: '_blog_session'

0 commit comments

Comments
 (0)