discourse/app/models/user_password.rb
Kelv 32e261ef73
DEV: Migrate user passwords data to UserPassword table (#28746)
* Add migrations to ensure password hash is synced across users & user_passwords

* Persist password-related data in user_passwords instead of users

* Merge User#expire_old_email_tokens with User#expire_tokens_if_password_changed

* Add post deploy migration to mark password-related columns from users table as read-only

* Refactored UserPassword#confirm_password? and changes required to accommodate hashing the password after validations
2024-10-10 09:23:06 +08:00

92 lines
2.7 KiB
Ruby

# 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)
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
#