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

Code before:
            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.
      def signed_id_verifier=(verifier)
        if signed_id_verifier_secret
          @signed_id_verifier = verifier
        else
          self._signed_id_verifier = verifier
        end
      end

      # :nodoc:
      def combine_signed_id_purposes(purpose)
        [ base_class.name.underscore, purpose.to_s ].compact_blank.join("/")
      end
    end


    # Returns a signed id that's generated using a preconfigured +ActiveSupport::MessageVerifier+ instance.
    #
    # This signed id is tamper proof, so it's safe to send in an email or otherwise share with the outside world.
    # However, as with any message signed with a +ActiveSupport::MessageVerifier+,
    # {the signed id is not encrypted}[link:classes/ActiveSupport/MessageVerifier.html#class-ActiveSupport::MessageVerifier-label-Signing+is+not+encryption].
    # It's just encoded and protected against tampering.
    #
    # This means that the ID can be decoded by anyone; however, if tampered with (so to point to a different ID),
    # the cryptographic signature will no longer match, and the signed id will be considered invalid and return nil
    # when passed to +find_signed+ (or raise with +find_signed!+).
    #
    # It can furthermore be set to expire (the default is not to expire), and scoped down with a specific purpose.
    # If the expiration date has been exceeded before +find_signed+ is called, the id won't find the designated
    # record. If a purpose is set, this too must match.
    #
    # If you accidentally let a signed id out in the wild that you wish to retract sooner than its expiration date

Review comment (suggestion):
We have a [test for setting `signed_id_verifier`](https://github.com/rails/rails/blob/065ba4faaff079b070bf349820d18a90baf1d53b/activerecord/test/cases/signed_id_test.rb#L186-L193), but I think we should add a test for the new `class_attribute` scoping of `signed_id_verifier` (i.e. that subclasses can inherit and override `signed_id_verifier`).

Code after:
            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.
      def signed_id_verifier=(verifier)
        if signed_id_verifier_secret
          @signed_id_verifier = verifier
        else
          self._signed_id_verifier = verifier
        end
      end

      # :nodoc:
      def combine_signed_id_purposes(purpose)
        [ base_class.name.underscore, purpose.to_s ].compact_blank.join("/")
      end
    end


    # Returns a signed id that's generated using a preconfigured +ActiveSupport::MessageVerifier+ instance.
    #
    # This signed id is tamper proof, so it's safe to send in an email or otherwise share with the outside world.
    # However, as with any message signed with a +ActiveSupport::MessageVerifier+,
    # {the signed id is not encrypted}[link:classes/ActiveSupport/MessageVerifier.html#class-ActiveSupport::MessageVerifier-label-Signing+is+not+encryption].
    # It's just encoded and protected against tampering.
    #
    # This means that the ID can be decoded by anyone; however, if tampered with (so to point to a different ID),
    # the cryptographic signature will no longer match, and the signed id will be considered invalid and return nil
    # when passed to +find_signed+ (or raise with +find_signed!+).
    #
    # It can furthermore be set to expire (the default is not to expire), and scoped down with a specific purpose.
    # If the expiration date has been exceeded before +find_signed+ is called, the id won't find the designated
    # record. If a purpose is set, this too must match.
    #
    # If you accidentally let a signed id out in the wild that you wish to retract sooner than its expiration date