# frozen_string_literal: true class UserPassword < ActiveRecord::Base MAX_PASSWORD_LENGTH = 200 TARGET_PASSWORD_ALGORITHM = "$pbkdf2-#{Rails.configuration.pbkdf2_algorithm}$i=#{Rails.configuration.pbkdf2_iterations},l=32$" PASSWORD_SALT_LENGTH = 16 belongs_to :user, required: true validates :user_id, uniqueness: true validate :password_validator before_save :ensure_password_is_hashed after_save :clear_raw_password def password # this getter method is still required, but we store the set password in @raw_password instead of making it easily accessible from the getter nil end def password=(pw) return if pw.blank? self.password_hash_will_change! @raw_password = pw end def password_validation_required? @raw_password.present? end def confirm_password?(pw) # nothing to confirm if this record has not been persisted yet return false if !persisted? return false if password_hash != hash_password(pw, password_salt, password_algorithm) regen_password!(pw) if password_algorithm != TARGET_PASSWORD_ALGORITHM true end private def clear_raw_password @raw_password = nil end def password_validator UserPasswordValidator.new(attributes: :password).validate_each(self, :password, @raw_password) end def hash_password(pw, salt, algorithm) raise StandardError.new("password is too long") if pw.size > MAX_PASSWORD_LENGTH PasswordHasher.hash_password(password: pw, salt: salt, algorithm: algorithm) end def ensure_password_is_hashed return if @raw_password.blank? self.password_salt = SecureRandom.hex(PASSWORD_SALT_LENGTH) self.password_algorithm = TARGET_PASSWORD_ALGORITHM self.password_hash = hash_password(@raw_password, password_salt, password_algorithm) self.password_expired_at = nil end def regen_password!(pw) # Regenerate password_hash with new algorithm and persist, we skip validation here since it has already run once when the hash was persisted the first time salt = SecureRandom.hex(PASSWORD_SALT_LENGTH) update_columns( password_algorithm: TARGET_PASSWORD_ALGORITHM, password_salt: salt, password_hash: hash_password(pw, salt, TARGET_PASSWORD_ALGORITHM), ) end end # == Schema Information # # Table name: user_passwords # # id :integer not null, primary key # user_id :integer not null # password_hash :string(64) not null # password_salt :string(32) not null # password_algorithm :string(64) not null # password_expired_at :datetime # created_at :datetime not null # updated_at :datetime not null # # Indexes # # index_user_passwords_on_user_id (user_id) UNIQUE #