feat: Email as a method for multi-factor authentication

pull/14832/head
halo 2025-01-14 15:22:57 +08:00
parent 8198620a2e
commit 557b58d815
6 changed files with 91 additions and 2 deletions

View File

@ -3,3 +3,4 @@ from .sms import MFASms
from .radius import MFARadius from .radius import MFARadius
from .custom import MFACustom from .custom import MFACustom
from .face import MFAFace from .face import MFAFace
from .email import MFAEmail

View File

@ -0,0 +1,80 @@
from django.conf import settings
from django.template.loader import render_to_string
from django.utils.translation import gettext_lazy as _
from common.utils import random_string
from common.utils.verify_code import SendAndVerifyCodeUtil
from settings.utils import get_login_title
from users.serializers import SmsUserSerializer
from .base import BaseMFA
otp_failed_msg = _("OTP code invalid, or server time error")
class MFAEmail(BaseMFA):
name = 'email'
display_name = _('Email')
placeholder = _('OTP verification code')
def __init__(self, user):
super().__init__(user)
self.email, self.user_info = '', None
if self.is_authenticated():
self.email = user.email
self.user_info = SmsUserSerializer(user).data
def check_code(self, code):
assert self.is_authenticated()
ok = False
msg = ''
try:
ok = self.email.verify(code)
except Exception as e:
msg = str(e)
return ok, msg
def is_active(self):
if not self.is_authenticated():
return True
return self.user.email
@staticmethod
def challenge_required():
return True
def send_challenge(self):
code = random_string(6, lower=False, upper=False)
subject = '%s: %s' % (get_login_title(), _('Forgot password'))
context = {
'user': self.user, 'title': subject, 'code': code,
}
subject = '%s: %s' % (get_login_title(), _('Forgot password'))
message = render_to_string('authentication/_msg_reset_password_code.html', context)
content = {'subject': subject, 'message': message}
self.email = SendAndVerifyCodeUtil(
self.email, code=code, backend=self.name, user_info=self.user_info, **content
)
self.email.gen_and_send_async()
@staticmethod
def global_enabled():
return settings.SECURITY_MFA_BY_EMAIL
def disable(self):
return '/ui/#/profile/index'
def get_enable_url(self) -> str:
return ''
def can_disable(self) -> bool:
return False
def get_disable_url(self):
return ''
@staticmethod
def help_text_of_enable():
return ''
def help_text_of_disable(self):
return ''

View File

@ -564,6 +564,7 @@ class Config(dict):
# 安全配置 # 安全配置
'SECURITY_MFA_AUTH': 0, # 0 不开启 1 全局开启 2 管理员开启 'SECURITY_MFA_AUTH': 0, # 0 不开启 1 全局开启 2 管理员开启
'SECURITY_MFA_AUTH_ENABLED_FOR_THIRD_PARTY': True, 'SECURITY_MFA_AUTH_ENABLED_FOR_THIRD_PARTY': True,
'SECURITY_MFA_BY_EMAIL': False,
'SECURITY_COMMAND_EXECUTION': False, 'SECURITY_COMMAND_EXECUTION': False,
'SECURITY_COMMAND_BLACKLIST': [ 'SECURITY_COMMAND_BLACKLIST': [
'reboot', 'shutdown', 'poweroff', 'halt', 'dd', 'half', 'top' 'reboot', 'shutdown', 'poweroff', 'halt', 'dd', 'half', 'top'

View File

@ -325,9 +325,10 @@ MFA_BACKEND_OTP = 'authentication.mfa.otp.MFAOtp'
MFA_BACKEND_FACE = 'authentication.mfa.face.MFAFace' MFA_BACKEND_FACE = 'authentication.mfa.face.MFAFace'
MFA_BACKEND_RADIUS = 'authentication.mfa.radius.MFARadius' MFA_BACKEND_RADIUS = 'authentication.mfa.radius.MFARadius'
MFA_BACKEND_SMS = 'authentication.mfa.sms.MFASms' MFA_BACKEND_SMS = 'authentication.mfa.sms.MFASms'
MFA_BACKEND_EMAIL = 'authentication.mfa.email.MFAEmail'
MFA_BACKEND_CUSTOM = 'authentication.mfa.custom.MFACustom' MFA_BACKEND_CUSTOM = 'authentication.mfa.custom.MFACustom'
MFA_BACKENDS = [MFA_BACKEND_OTP, MFA_BACKEND_RADIUS, MFA_BACKEND_SMS, MFA_BACKEND_FACE] MFA_BACKENDS = [MFA_BACKEND_OTP, MFA_BACKEND_RADIUS, MFA_BACKEND_SMS, MFA_BACKEND_FACE, MFA_BACKEND_EMAIL]
MFA_CUSTOM = CONFIG.MFA_CUSTOM MFA_CUSTOM = CONFIG.MFA_CUSTOM
MFA_CUSTOM_FILE_MD5 = CONFIG.MFA_CUSTOM_FILE_MD5 MFA_CUSTOM_FILE_MD5 = CONFIG.MFA_CUSTOM_FILE_MD5

View File

@ -34,6 +34,7 @@ FTP_FILE_MAX_STORE = CONFIG.FTP_FILE_MAX_STORE
# Security settings # Security settings
SECURITY_MFA_AUTH = CONFIG.SECURITY_MFA_AUTH SECURITY_MFA_AUTH = CONFIG.SECURITY_MFA_AUTH
SECURITY_MFA_AUTH_ENABLED_FOR_THIRD_PARTY = CONFIG.SECURITY_MFA_AUTH_ENABLED_FOR_THIRD_PARTY SECURITY_MFA_AUTH_ENABLED_FOR_THIRD_PARTY = CONFIG.SECURITY_MFA_AUTH_ENABLED_FOR_THIRD_PARTY
SECURITY_MFA_BY_EMAIL = CONFIG.SECURITY_MFA_BY_EMAIL
SECURITY_MAX_IDLE_TIME = CONFIG.SECURITY_MAX_IDLE_TIME # Unit: minute SECURITY_MAX_IDLE_TIME = CONFIG.SECURITY_MAX_IDLE_TIME # Unit: minute
SECURITY_MAX_SESSION_TIME = CONFIG.SECURITY_MAX_SESSION_TIME # Unit: hour SECURITY_MAX_SESSION_TIME = CONFIG.SECURITY_MAX_SESSION_TIME # Unit: hour
SECURITY_COMMAND_EXECUTION = CONFIG.SECURITY_COMMAND_EXECUTION SECURITY_COMMAND_EXECUTION = CONFIG.SECURITY_COMMAND_EXECUTION

View File

@ -124,6 +124,11 @@ class SecurityAuthSerializer(serializers.Serializer):
label=_('Third-party login MFA'), label=_('Third-party login MFA'),
help_text=_('The third-party login modes include OIDC, CAS, and SAML2'), help_text=_('The third-party login modes include OIDC, CAS, and SAML2'),
) )
SECURITY_MFA_BY_EMAIL = serializers.BooleanField(
required=False, default=False,
label=_('MFA via Email'),
help_text=_('Email as a method for multi-factor authentication')
)
OTP_ISSUER_NAME = serializers.CharField( OTP_ISSUER_NAME = serializers.CharField(
required=False, max_length=16, label=_('OTP issuer name'), required=False, max_length=16, label=_('OTP issuer name'),
) )