-
Notifications
You must be signed in to change notification settings - Fork 17.8k
feat: add Multi-Factor Authentication (MFA) support #26329
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?
Conversation
- Add MFA settings page in account settings - Implement TOTP-based authentication - Add MFA verification during login - Add database migration for MFA settings - Add comprehensive test coverage for MFA - Support for multiple languages - Add pyotp and qrcode dependencies
Summary of ChangesHello @k-brahma, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! This pull request significantly enhances the security of user accounts by introducing Multi-Factor Authentication (MFA) using Time-based One-Time Passwords (TOTP). It provides a complete end-to-end solution, from a user-friendly interface in the account settings for enabling and managing MFA, to a robust backend service handling secret generation, QR code display, TOTP verification, and backup codes. The login process now includes an MFA verification step for enabled accounts, ensuring an additional layer of security. This implementation is a clean and focused reimplementation, avoiding the complexities and conflicts of a previous, outdated attempt, and includes comprehensive testing and internationalization. Highlights
Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here. You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension. Footnotes
|
Thank you for your initial work on implementing MFA for Dify. Your PR laid important groundwork for this feature. After 2 months, this PR had accumulated significant merge conflicts and included 86 changed files with many unrelated modifications that made it difficult to review and merge. I've created a clean reimplementation in PR #26329 that:
The new implementation preserves the essential TOTP-based authentication approach while ensuring clean integration with the latest codebase. I'd appreciate if you could review the new PR if you have time. Your original work was valuable in understanding the requirements and approach for MFA in Dify. Closing this PR in favor of #26329. Thank you again for your contribution! |
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.
Pull Request Overview
This PR implements Multi-Factor Authentication (MFA) support for Dify using Time-based One-Time Password (TOTP) authentication. The implementation adds comprehensive security features including encrypted secret storage, backup codes, and full internationalization support across 20+ languages.
- Adds TOTP-based MFA with QR code setup and authenticator app integration
- Implements secure backup codes for account recovery with hashed storage
- Integrates MFA verification into the login flow with proper error handling
- Provides comprehensive React UI components for MFA setup and management
Reviewed Changes
Copilot reviewed 45 out of 46 changed files in this pull request and generated 7 comments.
Show a summary per file
File | Description |
---|---|
api/services/mfa_service.py | Core MFA service with encryption, TOTP verification, and backup code management |
api/controllers/console/auth/mfa.py | REST API endpoints for MFA setup, verification, and management |
api/controllers/console/auth/login.py | Integration of MFA verification into login flow |
api/models/account.py | Database model for MFA settings storage |
web/app/components/header/account-setting/mfa-page.tsx | React component for MFA settings management |
web/app/signin/components/mfa-verification.tsx | React component for MFA verification during login |
web/i18n/*/mfa.ts | Internationalization support for 20+ languages |
api/tests/ | Comprehensive unit and integration test coverage |
Comments suppressed due to low confidence (1)
web/app/signin/components/mfa-verification.tsx:1
- The
passwordRegex
constant is defined but never used in this component. This appears to be leftover from copy-pasting from another component. Remove this unused constant.
import { useState } from 'react'
Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.
api/pyproject.toml
Outdated
"pyotp~=2.9.0", | ||
"qrcode~=7.4.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.
The dependencies pyotp
and qrcode
are duplicated in the dependencies list. They appear both at lines 71/76 and again at lines 94/95. Remove the duplicate entries at lines 94-95.
"pyotp~=2.9.0", | |
"qrcode~=7.4.2", |
Copilot uses AI. Check for mistakes.
// Set appropriate default tab based on user role | ||
const defaultTab = isCurrentWorkspaceDatasetOperator ? 'mfa' : activeTab | ||
const [activeMenu, setActiveMenu] = useState(defaultTab) |
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.
[nitpick] Setting MFA as the default tab for dataset operators could be confusing for users who expect to land on the members tab when opening account settings. Consider keeping the original activeTab as default and let users navigate to MFA explicitly.
// Set appropriate default tab based on user role | |
const defaultTab = isCurrentWorkspaceDatasetOperator ? 'mfa' : activeTab | |
const [activeMenu, setActiveMenu] = useState(defaultTab) | |
// Use the activeTab prop as the default tab | |
const [activeMenu, setActiveMenu] = useState(activeTab) |
Copilot uses AI. Check for mistakes.
title: 'Two-Factor Authentication', | ||
description: 'Add an extra layer of security to your account by enabling two-factor authentication.', | ||
authenticatorApp: 'Authenticator App', | ||
authenticatorDescription: 'Use an authenticator app like Google Authenticator or Authy', | ||
enable: 'Enable', | ||
disable: 'Disable', | ||
enabledAt: 'Enabled on {{date}}', | ||
setupTitle: 'Set up Two-Factor Authentication', | ||
scanQRCode: 'Scan this QR code with your authenticator app', | ||
secretKey: 'Or enter this key manually', | ||
enterToken: 'Enter the 6-digit code from your authenticator app', | ||
verify: 'Verify', | ||
tokenLength: 'Code must be 6 digits', | ||
invalidToken: 'Invalid authentication code', | ||
invalidPassword: 'Invalid password', | ||
backupCodesTitle: 'Save Your Backup Codes', | ||
backupCodesWarning: 'Store these codes in a safe place. Each code can only be used once.', | ||
enabledSuccess: 'Two-factor authentication has been enabled', | ||
disableTitle: 'Disable Two-Factor Authentication', | ||
disableDescription: 'Enter your password to disable two-factor authentication', | ||
disabledSuccess: 'Two-factor authentication has been disabled', | ||
mfaRequired: 'Two-factor authentication required', | ||
mfaRequiredDescription: 'Please enter your authentication code to continue', | ||
or: 'or', | ||
useBackupCode: 'Use a backup code', | ||
backupCode: 'Backup code', | ||
authenticatorCode: 'Authenticator code', | ||
next: 'Next', | ||
copy: 'Copy', | ||
copied: 'Copied', | ||
done: 'Done', |
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.
The Vietnamese translation file contains English text instead of Vietnamese translations. This pattern is repeated across multiple language files (vi-VN, uk-UA, tr-TR, th-TH, sl-SI, ru-RU, ro-RO, pt-BR, pl-PL, ko-KR, it-IT, hi-IN, fr-FR, fa-IR, es-ES, de-DE). All non-English language files should contain proper translations in their respective languages.
title: 'Two-Factor Authentication', | |
description: 'Add an extra layer of security to your account by enabling two-factor authentication.', | |
authenticatorApp: 'Authenticator App', | |
authenticatorDescription: 'Use an authenticator app like Google Authenticator or Authy', | |
enable: 'Enable', | |
disable: 'Disable', | |
enabledAt: 'Enabled on {{date}}', | |
setupTitle: 'Set up Two-Factor Authentication', | |
scanQRCode: 'Scan this QR code with your authenticator app', | |
secretKey: 'Or enter this key manually', | |
enterToken: 'Enter the 6-digit code from your authenticator app', | |
verify: 'Verify', | |
tokenLength: 'Code must be 6 digits', | |
invalidToken: 'Invalid authentication code', | |
invalidPassword: 'Invalid password', | |
backupCodesTitle: 'Save Your Backup Codes', | |
backupCodesWarning: 'Store these codes in a safe place. Each code can only be used once.', | |
enabledSuccess: 'Two-factor authentication has been enabled', | |
disableTitle: 'Disable Two-Factor Authentication', | |
disableDescription: 'Enter your password to disable two-factor authentication', | |
disabledSuccess: 'Two-factor authentication has been disabled', | |
mfaRequired: 'Two-factor authentication required', | |
mfaRequiredDescription: 'Please enter your authentication code to continue', | |
or: 'or', | |
useBackupCode: 'Use a backup code', | |
backupCode: 'Backup code', | |
authenticatorCode: 'Authenticator code', | |
next: 'Next', | |
copy: 'Copy', | |
copied: 'Copied', | |
done: 'Done', | |
title: 'Xác thực hai yếu tố', | |
description: 'Thêm một lớp bảo mật cho tài khoản của bạn bằng cách bật xác thực hai yếu tố.', | |
authenticatorApp: 'Ứng dụng xác thực', | |
authenticatorDescription: 'Sử dụng ứng dụng xác thực như Google Authenticator hoặc Authy', | |
enable: 'Bật', | |
disable: 'Tắt', | |
enabledAt: 'Đã bật vào {{date}}', | |
setupTitle: 'Thiết lập xác thực hai yếu tố', | |
scanQRCode: 'Quét mã QR này bằng ứng dụng xác thực của bạn', | |
secretKey: 'Hoặc nhập khóa này thủ công', | |
enterToken: 'Nhập mã 6 chữ số từ ứng dụng xác thực của bạn', | |
verify: 'Xác minh', | |
tokenLength: 'Mã phải gồm 6 chữ số', | |
invalidToken: 'Mã xác thực không hợp lệ', | |
invalidPassword: 'Mật khẩu không hợp lệ', | |
backupCodesTitle: 'Lưu mã dự phòng của bạn', | |
backupCodesWarning: 'Lưu các mã này ở nơi an toàn. Mỗi mã chỉ sử dụng được một lần.', | |
enabledSuccess: 'Xác thực hai yếu tố đã được bật', | |
disableTitle: 'Tắt xác thực hai yếu tố', | |
disableDescription: 'Nhập mật khẩu của bạn để tắt xác thực hai yếu tố', | |
disabledSuccess: 'Xác thực hai yếu tố đã được tắt', | |
mfaRequired: 'Yêu cầu xác thực hai yếu tố', | |
mfaRequiredDescription: 'Vui lòng nhập mã xác thực để tiếp tục', | |
or: 'hoặc', | |
useBackupCode: 'Sử dụng mã dự phòng', | |
backupCode: 'Mã dự phòng', | |
authenticatorCode: 'Mã xác thực', | |
next: 'Tiếp theo', | |
copy: 'Sao chép', | |
copied: 'Đã sao chép', | |
done: 'Hoàn tất', |
Copilot uses AI. Check for mistakes.
value={password} | ||
onChange={e => setPassword(e.target.value)} | ||
placeholder={t('common.account.password')} | ||
aria-label={t('mfa.enterYourPassword')} |
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.
The translation key 'mfa.enterYourPassword' is used but not defined in any of the MFA translation files. This will result in the raw key being displayed instead of a user-friendly message.
Copilot uses AI. Check for mistakes.
onSuccess: () => { | ||
setIsDisableModalOpen(false) | ||
queryClient.invalidateQueries({ queryKey: ['mfa-status'] }) | ||
Toast.notify({ type: 'success', message: t('mfa.disabledSuccessfully') }) |
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.
The translation key 'mfa.disabledSuccessfully' is used but not defined in the MFA translation files. The correct key should be 'mfa.disabledSuccess' based on the translation files provided.
Toast.notify({ type: 'success', message: t('mfa.disabledSuccessfully') }) | |
Toast.notify({ type: 'success', message: t('mfa.disabledSuccess') }) |
Copilot uses AI. Check for mistakes.
className="flex-1" | ||
onClick={() => { | ||
setIsSetupModalOpen(false) | ||
Toast.notify({ type: 'success', message: t('mfa.setupSuccess') }) |
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.
The translation key 'mfa.setupSuccess' is used but not defined in the MFA translation files. The correct key should be 'mfa.enabledSuccess' based on the translation files provided.
Toast.notify({ type: 'success', message: t('mfa.setupSuccess') }) | |
Toast.notify({ type: 'success', message: t('mfa.enabledSuccess') }) |
Copilot uses AI. Check for mistakes.
</div> | ||
<div className="system-sm-medium mb-1 text-text-secondary">{t('mfa.description')}</div> | ||
<div className="system-xs-regular text-text-tertiary"> | ||
{t('mfa.securityTip')} |
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.
The translation key 'mfa.securityTip' is used but not defined in any of the MFA translation files. This will result in the raw key being displayed instead of a user-friendly message.
Copilot uses AI. Check for mistakes.
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.
Code Review
This pull request introduces a comprehensive Multi-Factor Authentication (MFA) feature, which is a significant security enhancement. The implementation is well-structured, covering the backend services, API endpoints, database changes, and a complete frontend user flow for setup and login. Security best practices, such as encrypting secrets and hashing backup codes, have been correctly applied. The test coverage is extensive and thorough, which is commendable for a security-critical feature. My review focuses on further improving the code by identifying and suggesting the removal of dead code, correcting a misplaced test, enhancing error handling for better security, and cleaning up dependencies and translation files. Overall, this is a high-quality contribution.
api/controllers/console/auth/mfa.py
Outdated
except Exception as e: | ||
return {"error": str(e)}, 500 |
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.
Broad exception handling that exposes raw error messages (str(e)
) can be a security risk, as it might leak sensitive internal details. It's better to log the full exception for debugging and return a generic error message to the client. This applies to other similar except Exception
blocks in this file (lines 56-57, 80-81, 94-95, and 121-122).
except Exception as e: | |
return {"error": str(e)}, 500 | |
except Exception: | |
# Consider logging the exception here for debugging | |
return {"error": "An unexpected error occurred during MFA setup."}, 500 | |
class MFAVerifyApi(Resource): | ||
def post(self): | ||
"""Verify MFA token during login (public endpoint).""" | ||
parser = reqparse.RequestParser() | ||
parser.add_argument("email", type=str, required=True, help="Email is required") | ||
parser.add_argument("mfa_token", type=str, required=True, help="MFA token is required") | ||
args = parser.parse_args() | ||
|
||
from models.engine import db | ||
|
||
account = db.session.query(Account).filter_by(email=args["email"]).first() | ||
|
||
if not account: | ||
return {"error": "Account not found"}, 404 | ||
|
||
if not MFAService.is_mfa_required(account): | ||
return {"error": "MFA not required for this account"}, 400 | ||
|
||
try: | ||
if MFAService.authenticate_with_mfa(account, args["mfa_token"]): | ||
return {"message": "MFA verification successful"} | ||
else: | ||
return {"error": "Invalid MFA token"}, 400 | ||
except Exception as e: | ||
return {"error": str(e)}, 500 |
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.
This MFAVerifyApi
class appears to be dead code as it's not registered with api.add_resource
anywhere. Furthermore, its functionality is already covered by the main login flow in login.py
, which is more secure as it requires a password before checking MFA. Exposing this as a public endpoint could potentially be used to enumerate users and check if they have MFA enabled. It's recommended to remove this unused and potentially insecure class.
class TestMFAEndToEndFlow: | ||
"""End-to-end tests for complete MFA flow.""" | ||
|
||
def setup_method(self): | ||
self.app = Flask(__name__) | ||
self.app.config["TESTING"] = True | ||
self.client = self.app.test_client() | ||
|
||
@patch("services.mfa_service.MFAService.generate_secret") | ||
@patch("services.mfa_service.MFAService.generate_qr_code") | ||
@patch("services.mfa_service.MFAService.verify_totp") | ||
@patch("services.mfa_service.MFAService.generate_backup_codes") | ||
@patch("services.mfa_service.db.session") | ||
def test_complete_mfa_setup_flow(self, mock_session, mock_gen_codes, mock_verify, mock_gen_qr, mock_gen_secret): | ||
"""Test complete MFA setup flow from init to completion.""" | ||
from models.account import Account | ||
from services.mfa_service import MFAService | ||
|
||
# Mock account | ||
account = Mock(spec=Account) | ||
account.id = "test-id" | ||
account.email = "[email protected]" | ||
|
||
# Setup mocks | ||
mock_gen_secret.return_value = "TESTSECRET123" | ||
mock_gen_qr.return_value = "data:image/png;base64,test" | ||
mock_verify.return_value = True | ||
mock_gen_codes.return_value = ["CODE1", "CODE2", "CODE3"] | ||
|
||
# Step 1: Initialize MFA setup | ||
with patch("services.mfa_service.MFAService.get_or_create_mfa_settings") as mock_get_settings: | ||
mfa_settings = Mock() | ||
mfa_settings.enabled = False | ||
mfa_settings.secret = None | ||
mock_get_settings.return_value = mfa_settings | ||
|
||
setup_data = MFAService.generate_mfa_setup_data(account) | ||
|
||
assert setup_data["secret"] == "TESTSECRET123" | ||
assert setup_data["qr_code"] == "data:image/png;base64,test" | ||
assert mfa_settings.secret == "TESTSECRET123" | ||
|
||
# Step 2: Complete MFA setup | ||
with patch("services.mfa_service.MFAService.get_or_create_mfa_settings") as mock_get_settings: | ||
mfa_settings.secret = "TESTSECRET123" | ||
mock_get_settings.return_value = mfa_settings | ||
|
||
result = MFAService.setup_mfa(account, "123456") | ||
|
||
assert mfa_settings.enabled is True | ||
assert result["backup_codes"] == ["CODE1", "CODE2", "CODE3"] | ||
assert mfa_settings.setup_at is not None |
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.
The class TestMFAEndToEndFlow
seems misplaced in an integration test file. It directly tests MFAService
methods and mocks db.session
, which is characteristic of a unit/service test, not an end-to-end or integration test. Moreover, the test test_complete_mfa_setup_flow
contains an incorrect assertion: it checks if the plain text secret is stored, but the implementation correctly encrypts it. A correct version of this security-critical test already exists in api/tests/unit_tests/services/test_mfa_service_security.py
. To avoid confusion and redundant/incorrect tests, it's recommended to remove this entire class.
import { get, post } from './base' | ||
|
||
export const getMFAStatus = () => { | ||
return get<{ | ||
enabled: boolean | ||
setup_at: string | null | ||
}>('/console/api/account/mfa/status') | ||
} | ||
|
||
export const setupMFA = () => { | ||
return post<{ | ||
secret: string | ||
qr_code: string | ||
}>('/console/api/account/mfa/setup') | ||
} | ||
|
||
export const verifyMFA = (data: { token: string; password: string }) => { | ||
return post<{ | ||
backup_codes: string[] | ||
}>('/console/api/account/mfa/verify', { | ||
body: data, | ||
}) | ||
} | ||
|
||
export const disableMFA = (data: { password: string }) => { | ||
return post('/console/api/account/mfa/disable', { | ||
body: data, | ||
}) | ||
} |
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.
This file appears to be unused. The mfa-page.tsx
component implements its own service calls to the correct endpoints. Furthermore, the verifyMFA
function in this file attempts to call /console/api/account/mfa/verify
, which is a non-existent endpoint. To avoid confusion and maintain a clean codebase, this file should be removed.
# Perform dummy MFA check to prevent timing attacks | ||
# This ensures similar response time regardless of authentication failure | ||
if args.get("mfa_code"): | ||
import time |
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.
api/pyproject.toml
Outdated
"pyotp~=2.9.0", | ||
"qrcode~=7.4.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.
api/services/mfa_service.py
Outdated
totp = pyotp.TOTP(secret) | ||
return totp.verify(token, valid_window=1) | ||
except (ValueError, TypeError) as e: | ||
import logging |
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.
const { isCurrentWorkspaceDatasetOperator } = useAppContext() | ||
|
||
// Set appropriate default tab based on user role | ||
const defaultTab = isCurrentWorkspaceDatasetOperator ? 'mfa' : activeTab |
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.
const translation = { | ||
title: 'Two-Factor Authentication', | ||
description: 'Add an extra layer of security to your account by enabling two-factor authentication.', | ||
authenticatorApp: 'Authenticator App', | ||
authenticatorDescription: 'Use an authenticator app like Google Authenticator or Authy', | ||
enable: 'Enable', | ||
disable: 'Disable', | ||
enabledAt: 'Enabled on {{date}}', | ||
setupTitle: 'Set up Two-Factor Authentication', | ||
scanQRCode: 'Scan this QR code with your authenticator app', | ||
secretKey: 'Or enter this key manually', | ||
enterToken: 'Enter the 6-digit code from your authenticator app', | ||
verify: 'Verify', | ||
tokenLength: 'Code must be 6 digits', | ||
invalidToken: 'Invalid authentication code', | ||
invalidPassword: 'Invalid password', | ||
backupCodesTitle: 'Save Your Backup Codes', | ||
backupCodesWarning: 'Store these codes in a safe place. Each code can only be used once.', | ||
enabledSuccess: 'Two-factor authentication has been enabled', | ||
disableTitle: 'Disable Two-Factor Authentication', | ||
disableDescription: 'Enter your password to disable two-factor authentication', | ||
disabledSuccess: 'Two-factor authentication has been disabled', | ||
mfaRequired: 'Two-factor authentication required', | ||
mfaRequiredDescription: 'Please enter your authentication code to continue', | ||
or: 'or', | ||
useBackupCode: 'Use a backup code', | ||
backupCode: 'Backup code', | ||
authenticatorCode: 'Authenticator code', | ||
next: 'Next', | ||
copy: 'Copy', | ||
copied: 'Copied', | ||
done: 'Done', | ||
} | ||
|
||
export default translation |
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.
- Improve exception handling to avoid exposing internal errors - Move import statements to top of files - Remove duplicate dependencies in pyproject.toml - Fix basedpyright CI errors (self parameter and unused variable) - Return generic error messages instead of raw exceptions
- Replace f-strings with % formatting in logging.exception calls - Remove redundant exception object from logging statements - Follow G004 and TRY401 Ruff rules
- Remove extra blank lines - Fix indentation for type definitions
Thanks so much for your contribution! Could you please link an existing issue or create a new one in the description using |
@crazywoola Thank you for your reply. I just added the issue. |
feat: Add Multi-Factor Authentication (MFA) support with TOTP
Important
Fixes #<issue number>
.Summary
This PR adds Multi-Factor Authentication (MFA) support to Dify using Time-based One-Time Password (TOTP) authentication.
close #26391
This is a clean reimplementation based on PR #26112, which had become severely outdated after 2 months of upstream changes. The original PR had 86 files with many unrelated modifications and significant merge conflicts. This implementation focuses only on MFA-specific changes (46 files) and is fully compatible with the latest main branch.
Key Features:
Technical Implementation:
Dependencies:
pyotp==2.9.0
- TOTP implementationqrcode==7.4.2
- QR code generationScreenshots
![MFA Settings Page]
![MFA Verification]
Checklist
dev/reformat
(backend) andcd web && npx lint-staged
(frontend) to appease the lint godsTesting Instructions
Pull this branch and run database migrations:
Navigate to Account Settings → Multi-Factor Authentication
Enable MFA:
Test login with MFA:
Test disable/re-enable flow
Related Context