## 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:
Tanmay Deep Sharma
2025-09-18 16:49:24 +02:00
committed by GitHub
parent f03a52bd77
commit 239c4dcb91
33 changed files with 1345 additions and 37 deletions

View File

@@ -1,24 +1,14 @@
class Widget::TokenService
class Widget::TokenService < BaseTokenService
DEFAULT_EXPIRY_DAYS = 180
pattr_initialize [:payload, :token]
def generate_token
JWT.encode payload_with_expiry, secret_key, 'HS256'
end
def decode_token
JWT.decode(
token, secret_key, true, algorithm: 'HS256'
).first.symbolize_keys
rescue StandardError
{}
JWT.encode(token_payload, secret_key, algorithm)
end
private
def payload_with_expiry
payload.merge(exp: exp, iat: iat)
def token_payload
(payload || {}).merge(exp: exp, iat: iat)
end
def iat
@@ -34,8 +24,4 @@ class Widget::TokenService
token_expiry_value = InstallationConfig.find_by(name: 'WIDGET_TOKEN_EXPIRY')&.value
(token_expiry_value.presence || DEFAULT_EXPIRY_DAYS).to_i
end
def secret_key
Rails.application.secret_key_base
end
end