Repository: rails/rails
File: activerecord/lib/active_record/signed_id.rb (Ruby)

Code before:
        if id = signed_id_verifier.verified(signed_id, purpose: combine_signed_id_purposes(purpose), **options)
          find_by primary_key => id
        end
      end

      # Works like find_signed, but will raise an +ActiveSupport::MessageVerifier::InvalidSignature+
      # exception if the +signed_id+ has either expired, has a purpose mismatch, is for another record,
      # or has been tampered with. It will also raise an +ActiveRecord::RecordNotFound+ exception if
      # the valid signed id can't find a record.
      #
      # === Examples
      #
      #   User.find_signed! "bad data" # => ActiveSupport::MessageVerifier::InvalidSignature
      #
      #   signed_id = User.first.signed_id
      #   User.first.destroy
      #   User.find_signed! signed_id # => ActiveRecord::RecordNotFound
      def find_signed!(signed_id, purpose: nil, on_rotation: nil)
        options = { on_rotation: on_rotation }.compact
        if id = signed_id_verifier.verify(signed_id, purpose: combine_signed_id_purposes(purpose), **options)
          find(id)
        end
      end

      def signed_id_verifier
        return self._signed_id_verifier if self._signed_id_verifier

        if signed_id_verifier_secret != ActiveRecord::Base.signed_id_verifier_secret
          secret = signed_id_verifier_secret
          secret = secret.call if secret.respond_to?(:call)

          if secret.nil?
            raise ArgumentError, "You must set ActiveRecord::Base.signed_id_verifier_secret to use signed ids"
          else
            self._signed_id_verifier = ActiveSupport::MessageVerifier.new secret, digest: "SHA256", serializer: JSON, url_safe: true
          end
        elsif ActiveRecord::Base.message_verifiers
          ActiveRecord::Base.message_verifiers["active_record/signed_id"]
        else
          nil
        end
      end

      # Allows you to pass in a custom verifier used for the signed ids. This also allows you to use different
      # verifiers for different classes. This is also helpful if you need to rotate keys, as you can prepare
      # your custom verifier for that in advance. See ActiveSupport::MessageVerifier for details.
      def signed_id_verifier=(verifier)
        self._signed_id_verifier = verifier
      end

      # :nodoc:

Review comment (security):
Using `class_attribute` for `signed_id_verifier` is not a bad idea, but it can also be a breaking change.  Consider the following code:

```ruby
class Post < ActiveRecord::Base; end
ActiveRecord::Base.signed_id_verifier = some_verifier
Post.signed_id_verifier_secret = "..."
```

Before: `Post.signed_id_verifier` would instantiate a new verifier using `Post.signed_id_verifier_secret`.

After: `Post.signed_id_verifier` will just be `ActiveRecord::Base.signed_id_verifier`.

Therefore, I think setting `signed_id_verifier_secret` should disable the new behavior.


Code after:
        options = { on_rotation: on_rotation }.compact
        if id = signed_id_verifier.verified(signed_id, purpose: combine_signed_id_purposes(purpose), **options)
          find_by primary_key => id
        end
      end

      # Works like find_signed, but will raise an +ActiveSupport::MessageVerifier::InvalidSignature+
      # exception if the +signed_id+ has either expired, has a purpose mismatch, is for another record,
      # or has been tampered with. It will also raise an +ActiveRecord::RecordNotFound+ exception if
      # the valid signed id can't find a record.
      #
      # === Examples
      #
      #   User.find_signed! "bad data" # => ActiveSupport::MessageVerifier::InvalidSignature
      #
      #   signed_id = User.first.signed_id
      #   User.first.destroy
      #   User.find_signed! signed_id # => ActiveRecord::RecordNotFound
      def find_signed!(signed_id, purpose: nil, on_rotation: nil)
        options = { on_rotation: on_rotation }.compact
        if id = signed_id_verifier.verify(signed_id, purpose: combine_signed_id_purposes(purpose), **options)
          find(id)
        end
      end

      def signed_id_verifier
        if signed_id_verifier_secret
          @signed_id_verifier ||= begin
            secret = signed_id_verifier_secret
            secret = secret.call if secret.respond_to?(:call)

            if secret.nil?
              raise ArgumentError, "You must set ActiveRecord::Base.signed_id_verifier_secret to use signed IDs"
            end

            ActiveSupport::MessageVerifier.new secret, digest: "SHA256", serializer: JSON, url_safe: true
          end
        else
          return _signed_id_verifier if _signed_id_verifier

          if ActiveRecord.message_verifiers.nil?
            raise "You must set ActiveRecord.message_verifiers to use signed IDs"
          end

          ActiveRecord.message_verifiers["active_record/signed_id"]
        end
      end

      # Allows you to pass in a custom verifier used for the signed ids. This also allows you to use different
      # verifiers for different classes. This is also helpful if you need to rotate keys, as you can prepare
      # your custom verifier for that in advance. See ActiveSupport::MessageVerifier for details.