diff options
Diffstat (limited to 'actionpack/lib/action_dispatch/middleware/cookies.rb')
-rw-r--r-- | actionpack/lib/action_dispatch/middleware/cookies.rb | 164 |
1 files changed, 100 insertions, 64 deletions
diff --git a/actionpack/lib/action_dispatch/middleware/cookies.rb b/actionpack/lib/action_dispatch/middleware/cookies.rb index f21d1d4ee5..5b914f293d 100644 --- a/actionpack/lib/action_dispatch/middleware/cookies.rb +++ b/actionpack/lib/action_dispatch/middleware/cookies.rb @@ -31,7 +31,7 @@ module ActionDispatch # # # Sets a signed cookie, which prevents users from tampering with its value. # # The cookie is signed by your app's <tt>config.secret_key_base</tt> value. - # # It can be read using the signed method <tt>cookies.signed[:key]</tt> + # # It can be read using the signed method <tt>cookies.signed[:name]</tt> # cookies.signed[:user_id] = current_user.id # # # Sets a "permanent" cookie (which expires in 20 years from now). @@ -53,13 +53,13 @@ module ActionDispatch # # Please note that if you specify a :domain when setting a cookie, you must also specify the domain when deleting the cookie: # - # cookies[:key] = { + # cookies[:name] = { # value: 'a yummy cookie', # expires: 1.year.from_now, # domain: 'domain.com' # } # - # cookies.delete(:key, domain: 'domain.com') + # cookies.delete(:name, domain: 'domain.com') # # The option symbols for setting cookies are: # @@ -70,7 +70,7 @@ module ActionDispatch # restrict to the domain level. If you use a schema like www.example.com # and want to share session with user.example.com set <tt>:domain</tt> # to <tt>:all</tt>. Make sure to specify the <tt>:domain</tt> option with - # <tt>:all</tt> again when deleting keys. + # <tt>:all</tt> again when deleting cookies. # # domain: nil # Does not sets cookie domain. (default) # domain: :all # Allow the cookie for the top most level @@ -117,6 +117,9 @@ module ActionDispatch # the cookie again. This is useful for creating cookies with values that the user is not supposed to change. If a signed # cookie was tampered with by the user (or a 3rd party), nil will be returned. # + # If +config.secret_key_base+ and +config.secret_token+ (deprecated) are both set, + # legacy cookies signed with the old key generator will be transparently upgraded. + # # This jar requires that you set a suitable secret for the verification on your app's +config.secret_key_base+. # # Example: @@ -126,23 +129,20 @@ module ActionDispatch # # cookies.signed[:discount] # => 45 def signed - @signed ||= begin - if @options[:upgrade_legacy_signed_cookie_jar] + @signed ||= + if @options[:upgrade_legacy_signed_cookies] UpgradeLegacySignedCookieJar.new(self, @key_generator, @options) else SignedCookieJar.new(self, @key_generator, @options) end - end - end - - # Only needed for supporting the +UpgradeSignatureToEncryptionCookieStore+, users and plugin authors should not use this - def signed_using_old_secret #:nodoc: - @signed_using_old_secret ||= SignedCookieJar.new(self, ActiveSupport::DummyKeyGenerator.new(@options[:secret_token]), @options) end # Returns a jar that'll automatically encrypt cookie values before sending them to the client and will decrypt them for read. # If the cookie was tampered with by the user (or a 3rd party), nil will be returned. # + # If +config.secret_key_base+ and +config.secret_token+ (deprecated) are both set, + # legacy cookies signed with the old key generator will be transparently upgraded. + # # This jar requires that you set a suitable secret for the verification on your app's +config.secret_key_base+. # # Example: @@ -152,7 +152,38 @@ module ActionDispatch # # cookies.encrypted[:discount] # => 45 def encrypted - @encrypted ||= EncryptedCookieJar.new(self, @key_generator, @options) + @encrypted ||= + if @options[:upgrade_legacy_signed_cookies] + UpgradeLegacyEncryptedCookieJar.new(self, @key_generator, @options) + else + EncryptedCookieJar.new(self, @key_generator, @options) + end + end + + # Returns the +signed+ or +encrypted jar, preferring +encrypted+ if +secret_key_base+ is set. + # Used by ActionDispatch::Session::CookieStore to avoid the need to introduce new cookie stores. + def signed_or_encrypted + @signed_or_encrypted ||= + if @options[:secret_key_base].present? + encrypted + else + signed + end + end + end + + module VerifyAndUpgradeLegacySignedMessage + def initialize(*args) + super + @legacy_verifier = ActiveSupport::MessageVerifier.new(@options[:secret_token]) + end + + def verify_and_upgrade_legacy_signed_message(name, signed_message) + @legacy_verifier.verify(signed_message).tap do |value| + self[name] = value + end + rescue ActiveSupport::MessageVerifier::InvalidSignature + nil end end @@ -179,7 +210,7 @@ module ActionDispatch encrypted_signed_cookie_salt: env[ENCRYPTED_SIGNED_COOKIE_SALT] || '', secret_token: env[SECRET_TOKEN], secret_key_base: env[SECRET_KEY_BASE], - upgrade_legacy_signed_cookie_jar: env[SECRET_TOKEN].present? && env[SECRET_KEY_BASE].present? + upgrade_legacy_signed_cookies: env[SECRET_TOKEN].present? && env[SECRET_KEY_BASE].present? } end @@ -249,7 +280,7 @@ module ActionDispatch # Sets the cookie named +name+. The second argument may be the very cookie # value, or a hash of options as documented above. - def []=(key, options) + def []=(name, options) if options.is_a?(Hash) options.symbolize_keys! value = options[:value] @@ -260,10 +291,10 @@ module ActionDispatch handle_options(options) - if @cookies[key.to_s] != value or options[:expires] - @cookies[key.to_s] = value - @set_cookies[key.to_s] = options - @delete_cookies.delete(key.to_s) + if @cookies[name.to_s] != value or options[:expires] + @cookies[name.to_s] = value + @set_cookies[name.to_s] = options + @delete_cookies.delete(name.to_s) end value @@ -272,24 +303,24 @@ module ActionDispatch # Removes the cookie on the client machine by setting the value to an empty string # and the expiration date in the past. Like <tt>[]=</tt>, you can pass in # an options hash to delete cookies with extra data such as a <tt>:path</tt>. - def delete(key, options = {}) - return unless @cookies.has_key? key.to_s + def delete(name, options = {}) + return unless @cookies.has_key? name.to_s options.symbolize_keys! handle_options(options) - value = @cookies.delete(key.to_s) - @delete_cookies[key.to_s] = options + value = @cookies.delete(name.to_s) + @delete_cookies[name.to_s] = options value end # Whether the given cookie is to be deleted by this CookieJar. # Like <tt>[]=</tt>, you can pass in an options hash to test if a # deletion applies to a specific <tt>:path</tt>, <tt>:domain</tt> etc. - def deleted?(key, options = {}) + def deleted?(name, options = {}) options.symbolize_keys! handle_options(options) - @delete_cookies[key.to_s] == options + @delete_cookies[name.to_s] == options end # Removes all cookies on the client machine by calling <tt>delete</tt> for each cookie @@ -311,7 +342,6 @@ module ActionDispatch self.always_write_cookie = false private - def write_cookie?(cookie) @secure || !cookie[:secure] || always_write_cookie end @@ -326,11 +356,11 @@ module ActionDispatch @options = options end - def [](key) + def [](name) @parent_jar[name.to_s] end - def []=(key, options) + def []=(name, options) if options.is_a?(Hash) options.symbolize_keys! else @@ -338,7 +368,7 @@ module ActionDispatch end options[:expires] = 20.years.from_now - @parent_jar[key] = options + @parent_jar[name] = options end end @@ -354,13 +384,11 @@ module ActionDispatch def [](name) if signed_message = @parent_jar[name] - @verifier.verify(signed_message) + verify(signed_message) end - rescue ActiveSupport::MessageVerifier::InvalidSignature - nil end - def []=(key, options) + def []=(name, options) if options.is_a?(Hash) options.symbolize_keys! options[:value] = @verifier.generate(options[:value]) @@ -369,8 +397,15 @@ module ActionDispatch end raise CookieOverflow if options[:value].size > MAX_COOKIE_SIZE - @parent_jar[key] = options + @parent_jar[name] = options end + + private + def verify(signed_message) + @verifier.verify(signed_message) + rescue ActiveSupport::MessageVerifier::InvalidSignature + nil + end end # UpgradeLegacySignedCookieJar is used instead of SignedCookieJar if @@ -378,39 +413,22 @@ module ActionDispatch # legacy cookies signed with the old dummy key generator and re-saves # them using the new key generator to provide a smooth upgrade path. class UpgradeLegacySignedCookieJar < SignedCookieJar #:nodoc: - def initialize(*args) - super - @legacy_verifier = ActiveSupport::MessageVerifier.new(@options[:secret_token]) - end + include VerifyAndUpgradeLegacySignedMessage def [](name) if signed_message = @parent_jar[name] - verify_signed_message(signed_message) || verify_and_upgrade_legacy_signed_message(name, signed_message) + verify(signed_message) || verify_and_upgrade_legacy_signed_message(name, signed_message) end end - - def verify_signed_message(signed_message) - @verifier.verify(signed_message) - rescue ActiveSupport::MessageVerifier::InvalidSignature - nil - end - - def verify_and_upgrade_legacy_signed_message(name, signed_message) - @legacy_verifier.verify(signed_message).tap do |value| - self[name] = value - end - rescue ActiveSupport::MessageVerifier::InvalidSignature - nil - end end class EncryptedCookieJar #:nodoc: include ChainedCookieJars def initialize(parent_jar, key_generator, options = {}) - if ActiveSupport::DummyKeyGenerator === key_generator - raise "Encrypted Cookies must be used in conjunction with config.secret_key_base." + - "Set config.secret_key_base in config/initializers/secret_token.rb" + if ActiveSupport::LegacyKeyGenerator === key_generator + raise "You didn't set config.secret_key_base, which is required for this cookie jar. " + + "Read the upgrade documentation to learn more about this new config option." end @parent_jar = parent_jar @@ -420,16 +438,13 @@ module ActionDispatch @encryptor = ActiveSupport::MessageEncryptor.new(secret, sign_secret) end - def [](key) - if encrypted_message = @parent_jar[key] - @encryptor.decrypt_and_verify(encrypted_message) + def [](name) + if encrypted_message = @parent_jar[name] + decrypt_and_verify(encrypted_message) end - rescue ActiveSupport::MessageVerifier::InvalidSignature, - ActiveSupport::MessageEncryptor::InvalidMessage - nil end - def []=(key, options) + def []=(name, options) if options.is_a?(Hash) options.symbolize_keys! else @@ -438,7 +453,28 @@ module ActionDispatch options[:value] = @encryptor.encrypt_and_sign(options[:value]) raise CookieOverflow if options[:value].size > MAX_COOKIE_SIZE - @parent_jar[key] = options + @parent_jar[name] = options + end + + private + def decrypt_and_verify(encrypted_message) + @encryptor.decrypt_and_verify(encrypted_message) + rescue ActiveSupport::MessageVerifier::InvalidSignature, ActiveSupport::MessageEncryptor::InvalidMessage + nil + end + end + + # UpgradeLegacyEncryptedCookieJar is used by ActionDispatch::Session::CookieStore + # instead of EncryptedCookieJar if config.secret_token and config.secret_key_base + # are both set. It reads legacy cookies signed with the old dummy key generator and + # encrypts and re-saves them using the new key generator to provide a smooth upgrade path. + class UpgradeLegacyEncryptedCookieJar < EncryptedCookieJar #:nodoc: + include VerifyAndUpgradeLegacySignedMessage + + def [](name) + if encrypted_or_signed_message = @parent_jar[name] + decrypt_and_verify(encrypted_or_signed_message) || verify_and_upgrade_legacy_signed_message(name, encrypted_or_signed_message) + end end end |