mirror of
https://github.com/lingble/chatwoot.git
synced 2025-10-29 10:12:34 +00:00
feat: MFA (#12290)
## 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>
This commit is contained in:
committed by
GitHub
parent
f03a52bd77
commit
239c4dcb91
@@ -68,6 +68,16 @@ module Chatwoot
|
||||
|
||||
# Disable PDF/video preview generation as we don't use them
|
||||
config.active_storage.previewers = []
|
||||
|
||||
# Active Record Encryption configuration
|
||||
# Required for MFA/2FA features - skip if not using encryption
|
||||
if ENV['ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY'].present?
|
||||
config.active_record.encryption.primary_key = ENV['ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY']
|
||||
config.active_record.encryption.deterministic_key = ENV.fetch('ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY', nil)
|
||||
config.active_record.encryption.key_derivation_salt = ENV.fetch('ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT', nil)
|
||||
config.active_record.encryption.support_unencrypted_data = true
|
||||
config.active_record.encryption.store_key_references = true
|
||||
end
|
||||
end
|
||||
|
||||
def self.config
|
||||
@@ -82,4 +92,16 @@ module Chatwoot
|
||||
# ref: https://www.rubydoc.info/stdlib/openssl/OpenSSL/SSL/SSLContext#DEFAULT_PARAMS-constant
|
||||
ENV['REDIS_OPENSSL_VERIFY_MODE'] == 'none' ? OpenSSL::SSL::VERIFY_NONE : OpenSSL::SSL::VERIFY_PEER
|
||||
end
|
||||
|
||||
def self.encryption_configured?
|
||||
# Check if proper encryption keys are configured
|
||||
# MFA/2FA features should only be enabled when proper keys are set
|
||||
ENV['ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY'].present? &&
|
||||
ENV['ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY'].present? &&
|
||||
ENV['ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT'].present?
|
||||
end
|
||||
|
||||
def self.mfa_enabled?
|
||||
encryption_configured?
|
||||
end
|
||||
end
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
|
||||
# Configure sensitive parameters which will be filtered from the log file.
|
||||
Rails.application.config.filter_parameters += [
|
||||
:password, :secret, :_key, :auth, :crypt, :salt, :certificate, :otp, :access, :private, :protected, :ssn
|
||||
:password, :secret, :_key, :auth, :crypt, :salt, :certificate, :otp, :access, :private, :protected, :ssn,
|
||||
:otp_secret, :otp_code, :backup_code, :mfa_token, :otp_backup_codes
|
||||
]
|
||||
|
||||
# Regex to filter all occurrences of 'token' in keys except for 'website_token'
|
||||
|
||||
@@ -83,12 +83,17 @@ class Rack::Attack
|
||||
end
|
||||
|
||||
# ### Prevent Brute-Force Login Attacks ###
|
||||
# Exclude MFA verification attempts from regular login throttling
|
||||
throttle('login/ip', limit: 5, period: 5.minutes) do |req|
|
||||
req.ip if req.path_without_extentions == '/auth/sign_in' && req.post?
|
||||
if req.path_without_extentions == '/auth/sign_in' && req.post? && req.params['mfa_token'].blank?
|
||||
# Skip if this is an MFA verification request
|
||||
req.ip
|
||||
end
|
||||
end
|
||||
|
||||
throttle('login/email', limit: 10, period: 15.minutes) do |req|
|
||||
if req.path_without_extentions == '/auth/sign_in' && req.post?
|
||||
# Skip if this is an MFA verification request
|
||||
if req.path_without_extentions == '/auth/sign_in' && req.post? && req.params['mfa_token'].blank?
|
||||
# ref: https://github.com/rack/rack-attack/issues/399
|
||||
# NOTE: This line used to throw ArgumentError /rails/action_mailbox/sendgrid/inbound_emails : invalid byte sequence in UTF-8
|
||||
# Hence placed in the if block
|
||||
@@ -114,6 +119,28 @@ class Rack::Attack
|
||||
req.ip if req.path_without_extentions == '/api/v1/profile/resend_confirmation' && req.post?
|
||||
end
|
||||
|
||||
## MFA throttling - prevent brute force attacks
|
||||
throttle('mfa_verification/ip', limit: 5, period: 1.minute) do |req|
|
||||
if req.path_without_extentions == '/api/v1/profile/mfa'
|
||||
req.ip if req.delete? # Throttle disable attempts
|
||||
elsif req.path_without_extentions.match?(%r{/api/v1/profile/mfa/(verify|backup_codes)})
|
||||
req.ip if req.post? # Throttle verify and backup_codes attempts
|
||||
end
|
||||
end
|
||||
|
||||
# Separate rate limiting for MFA verification attempts
|
||||
throttle('mfa_login/ip', limit: 10, period: 1.minute) do |req|
|
||||
req.ip if req.path_without_extentions == '/auth/sign_in' && req.post? && req.params['mfa_token'].present?
|
||||
end
|
||||
|
||||
throttle('mfa_login/token', limit: 10, period: 1.minute) do |req|
|
||||
if req.path_without_extentions == '/auth/sign_in' && req.post?
|
||||
# Track by MFA token to prevent brute force on a specific token
|
||||
mfa_token = req.params['mfa_token'].presence
|
||||
(mfa_token.presence)
|
||||
end
|
||||
end
|
||||
|
||||
## Prevent Brute-Force Signup Attacks ###
|
||||
throttle('accounts/ip', limit: 5, period: 30.minutes) do |req|
|
||||
req.ip if req.path_without_extentions == '/api/v1/accounts' && req.post?
|
||||
|
||||
@@ -103,6 +103,18 @@ en:
|
||||
invalid_value: Invalid value. The values provided for %{attribute_name} are invalid
|
||||
custom_attribute_definition:
|
||||
key_conflict: The provided key is not allowed as it might conflict with default attributes.
|
||||
mfa:
|
||||
already_enabled: MFA is already enabled
|
||||
not_enabled: MFA is not enabled
|
||||
invalid_code: Invalid verification code
|
||||
invalid_backup_code: Invalid backup code
|
||||
invalid_token: Invalid or expired MFA token
|
||||
invalid_credentials: Invalid credentials or verification code
|
||||
feature_unavailable: MFA feature is not available. Please configure encryption keys.
|
||||
profile:
|
||||
mfa:
|
||||
enabled: MFA enabled successfully
|
||||
disabled: MFA disabled successfully
|
||||
account_saml_settings:
|
||||
invalid_certificate: must be a valid X.509 certificate in PEM format
|
||||
reports:
|
||||
|
||||
@@ -334,6 +334,14 @@ Rails.application.routes.draw do
|
||||
post :resend_confirmation
|
||||
post :reset_access_token
|
||||
end
|
||||
|
||||
# MFA routes
|
||||
scope module: 'profile' do
|
||||
resource :mfa, controller: 'mfa', only: [:show, :create, :destroy] do
|
||||
post :verify
|
||||
post :backup_codes
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
resource :notification_subscriptions, only: [:create, :destroy]
|
||||
|
||||
Reference in New Issue
Block a user