mirror of
https://github.com/lingble/chatwoot.git
synced 2026-01-09 22:11:47 +00:00
## Linear: - https://github.com/chatwoot/chatwoot/issues/486 ## Description This PR implements Multi-Factor Authentication (MFA) support for user accounts, enhancing security by requiring a second form of verification during login. The feature adds TOTP (Time-based One-Time Password) authentication with QR code generation and backup codes for account recovery. ## Type of change - [ ] New feature (non-breaking change which adds functionality) ## How Has This Been Tested? - Added comprehensive RSpec tests for MFA controller functionality - Tested MFA setup flow with QR code generation - Verified OTP validation and backup code generation - Tested login flow with MFA enabled/disabled ## Checklist: - [ ] My code follows the style guidelines of this project - [ ] I have performed a self-review of my code - [ ] I have commented on my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [ ] My changes generate no new warnings - [ ] I have added tests that prove my fix is effective or that my feature works - [ ] New and existing unit tests pass locally with my changes - [ ] Any dependent changes have been merged and published in downstream modules --------- Co-authored-by: Pranav <pranav@chatwoot.com> Co-authored-by: Sojan Jose <sojan@pepalo.com> Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
89 lines
1.9 KiB
Ruby
89 lines
1.9 KiB
Ruby
class Mfa::ManagementService
|
|
pattr_initialize [:user!]
|
|
|
|
def enable_two_factor!
|
|
user.otp_secret = User.generate_otp_secret
|
|
user.save!
|
|
end
|
|
|
|
def disable_two_factor!
|
|
user.otp_secret = nil
|
|
user.otp_required_for_login = false
|
|
user.otp_backup_codes = nil
|
|
user.save!
|
|
end
|
|
|
|
def verify_and_activate!
|
|
ActiveRecord::Base.transaction do
|
|
user.update!(otp_required_for_login: true)
|
|
backup_codes_generated? ? nil : generate_backup_codes!
|
|
end
|
|
end
|
|
|
|
def two_factor_provisioning_uri
|
|
return nil if user.otp_secret.blank?
|
|
|
|
issuer = 'Chatwoot'
|
|
label = user.email
|
|
user.otp_provisioning_uri(label, issuer: issuer)
|
|
end
|
|
|
|
def generate_backup_codes!
|
|
codes = Array.new(10) { SecureRandom.hex(4).upcase }
|
|
user.otp_backup_codes = codes
|
|
user.save!
|
|
codes
|
|
end
|
|
|
|
def validate_backup_code!(code)
|
|
return false unless valid_backup_code_input?(code)
|
|
|
|
codes = user.otp_backup_codes
|
|
found_index = find_matching_code_index(codes, code)
|
|
|
|
return false if found_index.nil?
|
|
|
|
mark_code_as_used(codes, found_index)
|
|
end
|
|
|
|
private
|
|
|
|
def valid_backup_code_input?(code)
|
|
user.otp_backup_codes.present? && code.present?
|
|
end
|
|
|
|
def find_matching_code_index(codes, code)
|
|
found_index = nil
|
|
|
|
# Constant-time comparison to prevent timing attacks
|
|
codes.each_with_index do |stored_code, idx|
|
|
is_match = ActiveSupport::SecurityUtils.secure_compare(stored_code, code)
|
|
is_unused = stored_code != 'XXXXXXXX'
|
|
found_index = idx if is_match && is_unused
|
|
end
|
|
|
|
found_index
|
|
end
|
|
|
|
def mark_code_as_used(codes, index)
|
|
codes[index] = 'XXXXXXXX'
|
|
user.otp_backup_codes = codes
|
|
user.save!
|
|
true
|
|
end
|
|
|
|
public
|
|
|
|
def backup_codes_generated?
|
|
user.otp_backup_codes.present?
|
|
end
|
|
|
|
def mfa_enabled?
|
|
user.otp_required_for_login?
|
|
end
|
|
|
|
def two_factor_setup_pending?
|
|
user.otp_secret.present? && !user.otp_required_for_login?
|
|
end
|
|
end
|