lib/rodauth/features/disallow_password_reuse.rb
# frozen-string-literal: true module Rodauth Feature.define(:disallow_password_reuse, :DisallowPasswordReuse) do depends :login_password_requirements_base translatable_method :password_same_as_previous_password_message, "same as previous password" auth_value_method :previous_password_account_id_column, :account_id auth_value_method :previous_password_hash_column, :password_hash auth_value_method :previous_password_hash_table, :account_previous_password_hashes auth_value_method :previous_password_id_column, :id auth_value_method :previous_passwords_to_check, 6 auth_methods( :add_previous_password_hash, :password_doesnt_match_previous_password? ) def set_password(password) hash = super add_previous_password_hash(hash) hash end def add_previous_password_hash(hash) ds = previous_password_ds unless @dont_check_previous_password keep_before = ds.reverse(previous_password_id_column). limit(nil, previous_passwords_to_check). get(previous_password_id_column) if keep_before ds.where(Sequel.expr(previous_password_id_column) <= keep_before). delete end end # This should never raise uniqueness violations, as it uses a serial primary key ds.insert(previous_password_account_id_column=>account_id, previous_password_hash_column=>hash) end def password_meets_requirements?(password) super && (@dont_check_previous_password || password_doesnt_match_previous_password?(password)) end private def password_doesnt_match_previous_password?(password) match = if use_database_authentication_functions? salts = previous_password_ds. select_map([previous_password_id_column, Sequel.function(function_name(:rodauth_get_previous_salt), previous_password_id_column).as(:salt)]) return true if salts.empty? salts.any? do |hash_id, salt| database_function_password_match?(:rodauth_previous_password_hash_match, hash_id, password, salt) end else # :nocov: previous_password_ds.select_map(previous_password_hash_column).any? do |hash| password_hash_match?(hash, password) end # :nocov: end return true unless match @password_requirement_message = password_same_as_previous_password_message false end def after_close_account super if defined?(super) previous_password_ds.delete end def before_create_account_route super if defined?(super) @dont_check_previous_password = true end def before_verify_account_route super if defined?(super) @dont_check_previous_password = true end def after_create_account if account_password_hash_column && !(respond_to?(:verify_account_set_password?) && verify_account_set_password?) add_previous_password_hash(password_hash(param(password_param))) end super if defined?(super) end def previous_password_ds db[previous_password_hash_table].where(previous_password_account_id_column=>account_id) end end end