-
Notifications
You must be signed in to change notification settings - Fork 36
backup codes #10434
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
backup codes #10434
Conversation
default to: -> { | ||
emails = @user.email_address_with_name | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
default to: -> { | |
emails = @user.email_address_with_name | |
} | |
default to: -> { @user.email_address_with_name } |
@@ -59,7 +59,7 @@ class Login < ApplicationRecord | |||
event :mark_complete do | |||
transitions from: :incomplete, to: :complete do | |||
guard do | |||
authentication_factors_count == (user.use_two_factor_authentication? ? 2 : 1) | |||
authentication_factors_count >= (user.use_two_factor_authentication? && !authenticated_with_backup_code ? 2 : 1) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
authentication_factors_count >= (user.use_two_factor_authentication? && !authenticated_with_backup_code ? 2 : 1) | |
authentication_factors_count == (user.use_two_factor_authentication? ? 2 : 1) || authenticated_with_backup_code |
backup_codes.create!(hash: OpenSSL::KDF.pbkdf2_hmac(code + pepper, hash: "sha512", salt:, iterations: 20_000, length: 64).unpack1("H*"), salt: Base64.strict_encode64(salt)) | ||
rescue ActiveRecord::RecordInvalid | ||
# if the code is already in use, skip it | ||
next | ||
end | ||
codes << code | ||
end | ||
|
||
codes | ||
end | ||
|
||
def redeem_backup_code(code) | ||
found = nil | ||
pepper = Credentials.fetch(:BACKUP_CODE_PEPPER) | ||
# make sure we do not short circuit | ||
unused_backup_codes.each do |backup_code| | ||
hash = OpenSSL::KDF.pbkdf2_hmac(code + pepper, hash: "sha512", salt: Base64.decode64(backup_code.salt), iterations: 20_000, length: 64).unpack1("H*") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Create a class method on BackupCode for calculating a hash.
found = nil | ||
pepper = Credentials.fetch(:BACKUP_CODE_PEPPER) | ||
# make sure we do not short circuit | ||
unused_backup_codes.each do |backup_code| | ||
hash = OpenSSL::KDF.pbkdf2_hmac(code + pepper, hash: "sha512", salt: Base64.decode64(backup_code.salt), iterations: 20_000, length: 64).unpack1("H*") | ||
if ActiveSupport::SecurityUtils.secure_compare(hash, backup_code.hash) | ||
found = backup_code | ||
end | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If someone is brute forcing, the loop will already run its entirety. And is someone puts in a valid backup code, that means they've already gained access and we don't need to keep running the loop to prevent leaking timing information.
Let's instantly return when found.
found.transaction do | ||
found.mark_used! | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since we're only doing one write here, we don't need a transaction
found.transaction do | |
found.mark_used! | |
end | |
found.mark_used! |
end | ||
|
||
def disable_backup_codes | ||
backup_codes.where.not(aasm_state: [:used, :invalidated]).find_each &:mark_invalidated! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd suggest
def invalidate_backup_codes
backup_codes.where(aasm_state: :unsaved).destroy_all
backup_codes.where(aasm_state: :unused).find_each &:mark_invalidated!
end
create_table :user_backup_codes do |t| | ||
t.references :user, null: false, foreign_key: true | ||
t.string :aasm_state | ||
t.text :hash, null: false |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add a unique index
t.string :aasm_state | ||
t.text :hash, null: false | ||
t.text :salt, null: false | ||
t.datetime :deleted_at |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Drop acts as parnoid and this column
transitions from: :unused, to: :used | ||
|
||
after do | ||
case user.backup_codes.where(aasm_state: :unused).count |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
case user.backup_codes.where(aasm_state: :unused).count | |
case user.backup_codes.unused.size |
case user.backup_codes.where(aasm_state: :unused).count | ||
when 0 | ||
User::BackupCodeMailer.with(user_id: user.id).no_codes_remaining.deliver_now | ||
when 1..2 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
when 1..2 | |
when 1..3 |
Summary of the problem
Describe your changes