mirror of https://github.com/jumpserver/jumpserver
feat: Email as a method for multi-factor authentication
parent
8198620a2e
commit
557b58d815
|
@ -2,4 +2,5 @@ from .otp import MFAOtp, otp_failed_msg
|
||||||
from .sms import MFASms
|
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
|
||||||
|
|
|
@ -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 ''
|
|
@ -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'
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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'),
|
||||||
)
|
)
|
||||||
|
|
Loading…
Reference in New Issue