diff --git a/apps/authentication/mfa/__init__.py b/apps/authentication/mfa/__init__.py index 29d9e2937..77212ba9f 100644 --- a/apps/authentication/mfa/__init__.py +++ b/apps/authentication/mfa/__init__.py @@ -2,4 +2,5 @@ from .otp import MFAOtp, otp_failed_msg from .sms import MFASms from .radius import MFARadius from .custom import MFACustom -from .face import MFAFace \ No newline at end of file +from .face import MFAFace +from .email import MFAEmail diff --git a/apps/authentication/mfa/email.py b/apps/authentication/mfa/email.py new file mode 100644 index 000000000..1cff5f4e3 --- /dev/null +++ b/apps/authentication/mfa/email.py @@ -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 '' diff --git a/apps/jumpserver/conf.py b/apps/jumpserver/conf.py index 01af7ec96..db6c59e6a 100644 --- a/apps/jumpserver/conf.py +++ b/apps/jumpserver/conf.py @@ -564,6 +564,7 @@ class Config(dict): # 安全配置 'SECURITY_MFA_AUTH': 0, # 0 不开启 1 全局开启 2 管理员开启 'SECURITY_MFA_AUTH_ENABLED_FOR_THIRD_PARTY': True, + 'SECURITY_MFA_BY_EMAIL': False, 'SECURITY_COMMAND_EXECUTION': False, 'SECURITY_COMMAND_BLACKLIST': [ 'reboot', 'shutdown', 'poweroff', 'halt', 'dd', 'half', 'top' diff --git a/apps/jumpserver/settings/auth.py b/apps/jumpserver/settings/auth.py index e69471dd4..5dc636bbf 100644 --- a/apps/jumpserver/settings/auth.py +++ b/apps/jumpserver/settings/auth.py @@ -325,9 +325,10 @@ MFA_BACKEND_OTP = 'authentication.mfa.otp.MFAOtp' MFA_BACKEND_FACE = 'authentication.mfa.face.MFAFace' MFA_BACKEND_RADIUS = 'authentication.mfa.radius.MFARadius' MFA_BACKEND_SMS = 'authentication.mfa.sms.MFASms' +MFA_BACKEND_EMAIL = 'authentication.mfa.email.MFAEmail' 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_FILE_MD5 = CONFIG.MFA_CUSTOM_FILE_MD5 diff --git a/apps/jumpserver/settings/custom.py b/apps/jumpserver/settings/custom.py index 952c9bdd7..743d55a5a 100644 --- a/apps/jumpserver/settings/custom.py +++ b/apps/jumpserver/settings/custom.py @@ -34,6 +34,7 @@ FTP_FILE_MAX_STORE = CONFIG.FTP_FILE_MAX_STORE # Security settings SECURITY_MFA_AUTH = CONFIG.SECURITY_MFA_AUTH 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_SESSION_TIME = CONFIG.SECURITY_MAX_SESSION_TIME # Unit: hour SECURITY_COMMAND_EXECUTION = CONFIG.SECURITY_COMMAND_EXECUTION diff --git a/apps/settings/serializers/security.py b/apps/settings/serializers/security.py index 9cb972c0c..0ad66795a 100644 --- a/apps/settings/serializers/security.py +++ b/apps/settings/serializers/security.py @@ -124,6 +124,11 @@ class SecurityAuthSerializer(serializers.Serializer): label=_('Third-party login MFA'), 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( required=False, max_length=16, label=_('OTP issuer name'), )