diff --git a/activesupport/lib/active_support/message_verifier.rb b/activesupport/lib/active_support/message_verifier.rb index b7ddb4a13a..b4268cfc6e 100644 --- a/activesupport/lib/active_support/message_verifier.rb +++ b/activesupport/lib/active_support/message_verifier.rb @@ -11,71 +11,79 @@ module ActiveSupport # +MessageVerifier+ makes it easy to generate and verify messages which are # signed to prevent tampering. # + # In a Rails application, you can use +Rails.application.message_verifier+ + # to manage unique instances of verifiers for each use case. + # {Learn more}[link:classes/Rails/Application.html#method-i-message_verifier]. + # # This is useful for cases like remember-me tokens and auto-unsubscribe links # where the session store isn't suitable or available. # - # Remember Me: - # cookies[:remember_me] = @verifier.generate([@user.id, 2.weeks.from_now]) + # First, generate a signed message: + # cookies[:remember_me] = Rails.application.message_verifier(:remember_me).generate([@user.id, 2.weeks.from_now]) # - # In the authentication filter: + # Later verify that message: # - # id, time = @verifier.verify(cookies[:remember_me]) - # if Time.now < time + # id, time = Rails.application.message_verifier(:remember_me).verify(cookies[:remember_me]) + # if time.future? # self.current_user = User.find(id) # end # - # By default it uses Marshal to serialize the message. If you want to use - # another serialization method, you can set the serializer in the options - # hash upon initialization: + # === Confine messages to a specific purpose # - # @verifier = ActiveSupport::MessageVerifier.new('s3Krit', serializer: YAML) + # It's not recommended to use the same verifier for different purposes in your application. + # Doing so could allow a malicious actor to re-use a signed message to perform an unauthorized + # action. + # You can reduce this risk by confining signed messages to a specific +:purpose+. # - # +MessageVerifier+ creates HMAC signatures using SHA1 hash algorithm by default. - # If you want to use a different hash algorithm, you can change it by providing - # +:digest+ key as an option while initializing the verifier: - # - # @verifier = ActiveSupport::MessageVerifier.new('s3Krit', digest: 'SHA256') - # - # === Confining messages to a specific purpose - # - # By default any message can be used throughout your app. But they can also be - # confined to a specific +:purpose+. - # - # token = @verifier.generate("this is the chair", purpose: :login) + # token = @verifier.generate("signed message", purpose: :login) # # Then that same purpose must be passed when verifying to get the data back out: # - # @verifier.verified(token, purpose: :login) # => "this is the chair" + # @verifier.verified(token, purpose: :login) # => "signed message" # @verifier.verified(token, purpose: :shipping) # => nil # @verifier.verified(token) # => nil # - # @verifier.verify(token, purpose: :login) # => "this is the chair" - # @verifier.verify(token, purpose: :shipping) # => ActiveSupport::MessageVerifier::InvalidSignature - # @verifier.verify(token) # => ActiveSupport::MessageVerifier::InvalidSignature + # @verifier.verify(token, purpose: :login) # => "signed message" + # @verifier.verify(token, purpose: :shipping) # => raises ActiveSupport::MessageVerifier::InvalidSignature + # @verifier.verify(token) # => raises ActiveSupport::MessageVerifier::InvalidSignature # # Likewise, if a message has no purpose it won't be returned when verifying with # a specific purpose. # - # token = @verifier.generate("the conversation is lively") - # @verifier.verified(token, purpose: :scare_tactics) # => nil - # @verifier.verified(token) # => "the conversation is lively" + # token = @verifier.generate("signed message") + # @verifier.verified(token, purpose: :redirect) # => nil + # @verifier.verified(token) # => "signed message" # - # @verifier.verify(token, purpose: :scare_tactics) # => ActiveSupport::MessageVerifier::InvalidSignature - # @verifier.verify(token) # => "the conversation is lively" + # @verifier.verify(token, purpose: :redirect) # => raises ActiveSupport::MessageVerifier::InvalidSignature + # @verifier.verify(token) # => "signed message" # - # === Making messages expire + # === Expiring messages # # By default messages last forever and verifying one year from now will still # return the original value. But messages can be set to expire at a given # time with +:expires_in+ or +:expires_at+. # - # @verifier.generate("parcel", expires_in: 1.month) - # @verifier.generate("doowad", expires_at: Time.now.end_of_year) + # @verifier.generate("signed message", expires_in: 1.month) + # @verifier.generate("signed message", expires_at: Time.now.end_of_year) # - # Then the messages can be verified and returned up to the expire time. + # Messages can then be verified and returned until expiry. # Thereafter, the +verified+ method returns +nil+ while +verify+ raises # ActiveSupport::MessageVerifier::InvalidSignature. # + # === Alternative serializers + # + # By default MessageVerifier uses Marshal to serialize the message. If you want to use + # another serialization method, you can set the serializer in the options + # hash upon initialization: + # + # @verifier = ActiveSupport::MessageVerifier.new("secret", serializer: YAML) + # + # +MessageVerifier+ creates HMAC signatures using the SHA1 hash algorithm by default. + # If you want to use a different hash algorithm, you can change it by providing + # +:digest+ key as an option while initializing the verifier: + # + # @verifier = ActiveSupport::MessageVerifier.new("secret", digest: "SHA256") + # # === Rotating keys # # MessageVerifier also supports rotating out old configurations by falling @@ -92,13 +100,13 @@ module ActiveSupport # Then gradually rotate the old values out by adding them as fallbacks. Any message # generated with the old values will then work until the rotation is removed. # - # verifier.rotate old_secret # Fallback to an old secret instead of @secret. - # verifier.rotate digest: "SHA256" # Fallback to an old digest instead of SHA512. - # verifier.rotate serializer: Marshal # Fallback to an old serializer instead of JSON. + # verifier.rotate(old_secret) # Fallback to an old secret instead of @secret. + # verifier.rotate(digest: "SHA256") # Fallback to an old digest instead of SHA512. + # verifier.rotate(serializer: Marshal) # Fallback to an old serializer instead of JSON. # # Though the above would most likely be combined into one rotation: # - # verifier.rotate old_secret, digest: "SHA256", serializer: Marshal + # verifier.rotate(old_secret, digest: "SHA256", serializer: Marshal) class MessageVerifier prepend Messages::Rotator::Verifier @@ -117,8 +125,8 @@ module ActiveSupport # Checks if a signed message could have been generated by signing an object # with the +MessageVerifier+'s secret. # - # verifier = ActiveSupport::MessageVerifier.new 's3Krit' - # signed_message = verifier.generate 'a private message' + # verifier = ActiveSupport::MessageVerifier.new("secret") + # signed_message = verifier.generate("signed message") # verifier.valid_message?(signed_message) # => true # # tampered_message = signed_message.chop # editing the message invalidates the signature @@ -130,14 +138,14 @@ module ActiveSupport # Decodes the signed message using the +MessageVerifier+'s secret. # - # verifier = ActiveSupport::MessageVerifier.new 's3Krit' + # verifier = ActiveSupport::MessageVerifier.new("secret") # - # signed_message = verifier.generate 'a private message' - # verifier.verified(signed_message) # => 'a private message' + # signed_message = verifier.generate("signed message") + # verifier.verified(signed_message) # => "signed message" # # Returns +nil+ if the message was not signed with the same secret. # - # other_verifier = ActiveSupport::MessageVerifier.new 'd1ff3r3nt-s3Krit' + # other_verifier = ActiveSupport::MessageVerifier.new("different_secret") # other_verifier.verified(signed_message) # => nil # # Returns +nil+ if the message is not Base64-encoded. @@ -164,15 +172,15 @@ module ActiveSupport # Decodes the signed message using the +MessageVerifier+'s secret. # - # verifier = ActiveSupport::MessageVerifier.new 's3Krit' - # signed_message = verifier.generate 'a private message' + # verifier = ActiveSupport::MessageVerifier.new("secret") + # signed_message = verifier.generate("signed message") # - # verifier.verify(signed_message) # => 'a private message' + # verifier.verify(signed_message) # => "signed message" # # Raises +InvalidSignature+ if the message was not signed with the same # secret or was not Base64-encoded. # - # other_verifier = ActiveSupport::MessageVerifier.new 'd1ff3r3nt-s3Krit' + # other_verifier = ActiveSupport::MessageVerifier.new("different_secret") # other_verifier.verify(signed_message) # => ActiveSupport::MessageVerifier::InvalidSignature def verify(*args, **options) verified(*args, **options) || raise(InvalidSignature) @@ -183,8 +191,8 @@ module ActiveSupport # The message is signed with the +MessageVerifier+'s secret. # Returns Base64-encoded message joined with the generated signature. # - # verifier = ActiveSupport::MessageVerifier.new 's3Krit' - # verifier.generate 'a private message' # => "BAhJIhRwcml2YXRlLW1lc3NhZ2UGOgZFVA==--e2d724331ebdee96a10fb99b089508d1c72bd772" + # verifier = ActiveSupport::MessageVerifier.new("secret") + # verifier.generate("signed message") # => "BAhJIhNzaWduZWQgbWVzc2FnZQY6BkVU--f67d5f27c3ee0b8483cebf2103757455e947493b" def generate(value, expires_at: nil, expires_in: nil, purpose: nil) data = encode(Messages::Metadata.wrap(@serializer.dump(value), expires_at: expires_at, expires_in: expires_in, purpose: purpose)) "#{data}#{SEPARATOR}#{generate_digest(data)}"