Repository: rails/rails
File: activemodel/lib/active_model/secure_password.rb (Ruby)

Code before:
      def has_secure_password(attribute = :password, validations: true, reset_token: true)
        # Load bcrypt gem only when has_secure_password is used.
        # This is to avoid ActiveModel (and by extension the entire framework)
        # being dependent on a binary library.
        begin
          require "bcrypt"
        rescue LoadError
          warn "You don't have bcrypt installed in your application. Please add it to your Gemfile and run bundle install."
          raise
        end

        include InstanceMethodsOnActivation.new(attribute, reset_token: reset_token)

        if validations
          include ActiveModel::Validations

          # This ensures the model has a password by checking whether the password_digest
          # is present, so that this works with both new and existing records. However,
          # when there is an error, the message is added to the password attribute instead
          # so that the error message will make sense to the end-user.
          validate do |record|
            record.errors.add(attribute, :blank) unless record.public_send("#{attribute}_digest").present?
          end

          validate do |record|
            if challenge = record.public_send(:"#{attribute}_challenge")
              digest_was = record.public_send(:"#{attribute}_digest_was") if record.respond_to?(:"#{attribute}_digest_was")

              unless digest_was.present? && BCrypt::Password.new(digest_was).is_password?(challenge)
                record.errors.add(:"#{attribute}_challenge")
              end
            end
          end

          # Validates that the password does not exceed the maximum allowed bytes for BCrypt (72 bytes).
          validate do |record|
            password_value = record.public_send(attribute)
            if password_value.present? && password_value.bytesize > ActiveModel::SecurePassword::MAX_PASSWORD_LENGTH_ALLOWED
              record.errors.add(attribute, :password_too_long)
            end
          end

          validates_confirmation_of attribute, allow_nil: true
        end

        # Only generate tokens for records that are capable of doing so (Active Records, not vanilla Active Models)
        if reset_token && respond_to?(:generates_token_for)
          reset_token_expires_in = reset_token.is_a?(Hash) ? reset_token[:expires_in] : DEFAULT_RESET_TOKEN_EXPIRES_IN

          silence_redefinition_of_method(:"#{attribute}_reset_token_expires_in")
          define_method(:"#{attribute}_reset_token_expires_in") { reset_token_expires_in }

Review comment (suggestion):
Although Argon2 apparently doesn't need any validation, it would be nice to show how a validation should be implemented.

Code after:
      #   # raises ActiveSupport::MessageVerifier::InvalidSignature since the token is expired
      #   User.find_by_password_reset_token!(token)
      #
      # ===== Customizing the hashing algorithm
      # To add a custom algorithm, create a class that implements +hash_password+, +verify_password+, +password_salt+,
      # +validate+ and +algorithm_name+ methods.
      #
      #   class Argon2Password
      #     def initialize
      #       require "argon2"
      #     end
      #
      #     def hash_password(unencrypted_password)
      #       Argon2::Password.create(unencrypted_password)
      #     end
      #
      #     def verify_password(password, digest)
      #       Argon2::Password.verify_password(password, digest)
      #     end
      #
      #     def password_salt(digest)
      #       Argon2::HashFormat.new(digest).salt
      #     end
      #
      #     def validate(_record, _attribute)
      #       # Argon2 has no maximum input size, no validation needed
      #     end
      #
      #     def algorithm_name
      #       :argon2
      #     end
      #   end
      #
      #   class User < ActiveRecord::Base
      #     has_secure_password algorithm: Argon2Password.new
      #   end
      #
      #   user = User.new(name: "david", password: "", password_confirmation: "nomatch")
      #   user.password_algorithm                                        # => :argon2
      #   user.password_digest                                           # => "$argon2id$v=19$m=65536,t=3,p=4$/hM+NKjC0FTYDpN3LNTzBQ$5V1T31fHB57+Y8G9TUkqSUMgEkSygv8AW/AHNfOm2ts"
      def has_secure_password(attribute = :password, validations: true, reset_token: true, algorithm: nil)
        # Load bcrypt gem only when has_secure_password is used.
        # This is to avoid ActiveModel (and by extension the entire framework)
        # being dependent on a binary library.
        algorithm ||= BCryptPassword.new

        include InstanceMethodsOnActivation.new(attribute, reset_token: reset_token, algorithm: algorithm)

        if validations
          include ActiveModel::Validations