diff --git a/app/forms/backup_code_verification_form.rb b/app/forms/backup_code_verification_form.rb index 3ca8e9b6cc2..2254e3db070 100644 --- a/app/forms/backup_code_verification_form.rb +++ b/app/forms/backup_code_verification_form.rb @@ -19,18 +19,19 @@ def submit(params) attr_reader :user, :backup_code def valid_backup_code? - backup_code_config.present? + valid_backup_code_config_created_at.present? end - def backup_code_config - @backup_code_config ||= BackupCodeGenerator.new(@user). - if_valid_consume_code_return_config(backup_code) + def valid_backup_code_config_created_at + return @valid_backup_code_config_created_at if defined?(@valid_backup_code_config_created_at) + @valid_backup_code_config_created_at = BackupCodeGenerator.new(@user). + if_valid_consume_code_return_config_created_at(backup_code) end def extra_analytics_attributes { multi_factor_auth_method: 'backup_code', - multi_factor_auth_method_created_at: backup_code_config&.created_at&.strftime('%s%L'), + multi_factor_auth_method_created_at: valid_backup_code_config_created_at&.strftime('%s%L'), } end end diff --git a/app/models/backup_code_configuration.rb b/app/models/backup_code_configuration.rb index 1fc4e5e8600..4a744e885d2 100644 --- a/app/models/backup_code_configuration.rb +++ b/app/models/backup_code_configuration.rb @@ -35,18 +35,21 @@ class << self def find_with_code(code:, user_id:) return if code.blank? code = RandomPhrase.normalize(code) + user_salted_fingerprints = self.salted_fingerprints(code: code, user_id: user_id) + where(salted_code_fingerprint: user_salted_fingerprints).find_by(user_id: user_id) + end + + def salted_fingerprints(code:, user_id:) user_salt_costs = select(:code_salt, :code_cost). distinct. where(user_id: user_id). where.not(code_salt: nil).where.not(code_cost: nil). pluck(:code_salt, :code_cost) - salted_fingerprints = user_salt_costs.map do |salt, cost| + user_salt_costs.map do |salt, cost| scrypt_password_digest(password: code, salt: salt, cost: cost) end - - where(salted_code_fingerprint: salted_fingerprints).find_by(user_id: user_id) end def scrypt_password_digest(password:, salt:, cost:) diff --git a/app/services/backup_code_generator.rb b/app/services/backup_code_generator.rb index 575d3f9e019..199607bb05e 100644 --- a/app/services/backup_code_generator.rb +++ b/app/services/backup_code_generator.rb @@ -26,17 +26,33 @@ def create # @return [Boolean] def verify(plaintext_code) - if_valid_consume_code_return_config(plaintext_code).present? + if_valid_consume_code_return_config_created_at(plaintext_code).present? end # @return [BackupCodeConfiguration, nil] - def if_valid_consume_code_return_config(plaintext_code) + def if_valid_consume_code_return_config_created_at(plaintext_code) return unless plaintext_code.present? backup_code = RandomPhrase.normalize(plaintext_code) - config = BackupCodeConfiguration.find_with_code(code: backup_code, user_id: @user.id) - return unless code_usable?(config) - config.update!(used_at: Time.zone.now) - config + return nil unless backup_code + + salted_fingerprints = + BackupCodeConfiguration.salted_fingerprints(code: backup_code, user_id: @user.id) + + query_result = BackupCodeConfiguration.transaction do + sql = <<~SQL + UPDATE backup_code_configurations + SET + used_at = NOW() + WHERE user_id = ? AND salted_code_fingerprint IN (?) AND used_at IS NULL + RETURNING created_at; + SQL + query = BackupCodeConfiguration.sanitize_sql_array( + [sql, @user.id, salted_fingerprints], + ) + BackupCodeConfiguration.connection.execute(query).first + end + + query_result['created_at'] if query_result end def delete_existing_codes