Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 6 additions & 5 deletions app/forms/backup_code_verification_form.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
9 changes: 6 additions & 3 deletions app/models/backup_code_configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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:)
Expand Down
28 changes: 22 additions & 6 deletions app/services/backup_code_generator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down