mirror of
https://github.com/jumpserver/jumpserver.git
synced 2025-09-16 15:28:38 +00:00
feat: 首页的 chanlege 和 MFA 统一 (#6989)
* feat: 首页的 chanlege 和 MFA 统一 * 登陆样式调整 * mfa bug * q * m * mfa封装组件 前端可修改配置 * perf: 添加翻译 * login css bug * perf: 修改一些风格 * perf: 修改命名 * perf: 修改 mfa code 不是必填 * mfa 前端统一组件 * stash * perf: 统一验证码 Co-authored-by: feng626 <1304903146@qq.com> Co-authored-by: ibuler <ibuler@qq.com>
This commit is contained in:
@@ -2,17 +2,17 @@
|
||||
#
|
||||
import builtins
|
||||
import time
|
||||
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.conf import settings
|
||||
from django.shortcuts import get_object_or_404
|
||||
from rest_framework.permissions import AllowAny
|
||||
from rest_framework.generics import CreateAPIView
|
||||
from rest_framework.serializers import ValidationError
|
||||
from rest_framework.response import Response
|
||||
|
||||
from authentication.sms_verify_code import VerifyCodeUtil
|
||||
from common.exceptions import JMSException
|
||||
from common.permissions import IsValidUser, NeedMFAVerify, IsAppUser
|
||||
from users.models.user import MFAType
|
||||
from common.permissions import IsValidUser, NeedMFAVerify
|
||||
from users.models.user import MFAType, User
|
||||
from ..serializers import OtpVerifySerializer
|
||||
from .. import serializers
|
||||
from .. import errors
|
||||
@@ -90,6 +90,13 @@ class SendSMSVerifyCodeApi(AuthMixin, CreateAPIView):
|
||||
permission_classes = (AllowAny,)
|
||||
|
||||
def create(self, request, *args, **kwargs):
|
||||
user = self.get_user_from_session()
|
||||
username = request.data.get('username', '')
|
||||
username = username.strip()
|
||||
if username:
|
||||
user = get_object_or_404(User, username=username)
|
||||
else:
|
||||
user = self.get_user_from_session()
|
||||
if not user.mfa_enabled:
|
||||
raise errors.NotEnableMFAError
|
||||
timeout = user.send_sms_code()
|
||||
return Response({'code': 'ok','timeout': timeout})
|
||||
return Response({'code': 'ok', 'timeout': timeout})
|
||||
|
@@ -78,6 +78,7 @@ mfa_type_failed_msg = _(
|
||||
|
||||
mfa_required_msg = _("MFA required")
|
||||
mfa_unset_msg = _("MFA not set, please set it first")
|
||||
otp_unset_msg = _("OTP not set, please set it first")
|
||||
login_confirm_required_msg = _("Login confirm required")
|
||||
login_confirm_wait_msg = _("Wait login confirm ticket for accept")
|
||||
login_confirm_error_msg = _("Login confirm ticket was {}")
|
||||
@@ -354,3 +355,15 @@ class NotHaveUpDownLoadPerm(JMSException):
|
||||
status_code = status.HTTP_403_FORBIDDEN
|
||||
code = 'not_have_up_down_load_perm'
|
||||
default_detail = _('No upload or download permission')
|
||||
|
||||
|
||||
class NotEnableMFAError(JMSException):
|
||||
default_detail = mfa_unset_msg
|
||||
|
||||
|
||||
class OTPRequiredError(JMSException):
|
||||
default_detail = otp_unset_msg
|
||||
|
||||
def __init__(self, url, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.url = url
|
||||
|
@@ -43,7 +43,7 @@ class UserLoginForm(forms.Form):
|
||||
|
||||
|
||||
class UserCheckOtpCodeForm(forms.Form):
|
||||
code = forms.CharField(label=_('MFA Code'), max_length=6)
|
||||
code = forms.CharField(label=_('MFA Code'), max_length=6, required=False)
|
||||
mfa_type = forms.CharField(label=_('MFA type'), max_length=6)
|
||||
|
||||
|
||||
@@ -59,7 +59,7 @@ class ChallengeMixin(forms.Form):
|
||||
challenge = forms.CharField(
|
||||
label=_('MFA code'), max_length=6, required=False,
|
||||
widget=forms.TextInput(attrs={
|
||||
'placeholder': _("MFA code"),
|
||||
'placeholder': _("Dynamic code"),
|
||||
'style': 'width: 50%'
|
||||
})
|
||||
)
|
||||
@@ -69,6 +69,8 @@ def get_user_login_form_cls(*, captcha=False):
|
||||
bases = []
|
||||
if settings.SECURITY_LOGIN_CHALLENGE_ENABLED:
|
||||
bases.append(ChallengeMixin)
|
||||
elif settings.SECURITY_MFA_IN_LOGIN_PAGE:
|
||||
bases.append(UserCheckOtpCodeForm)
|
||||
elif settings.SECURITY_LOGIN_CAPTCHA_ENABLED and captcha:
|
||||
bases.append(CaptchaMixin)
|
||||
bases.append(UserLoginForm)
|
||||
|
@@ -7,6 +7,7 @@ import time
|
||||
|
||||
from django.core.cache import cache
|
||||
from django.conf import settings
|
||||
from django.urls import reverse_lazy
|
||||
from django.contrib import auth
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.contrib.auth import (
|
||||
@@ -14,11 +15,11 @@ from django.contrib.auth import (
|
||||
PermissionDenied, user_login_failed, _clean_credentials
|
||||
)
|
||||
from django.shortcuts import reverse, redirect
|
||||
from django.views.generic.edit import FormView
|
||||
|
||||
from common.utils import get_object_or_none, get_request_ip, get_logger, bulk_get, FlashMessageUtil
|
||||
from users.models import User, MFAType
|
||||
from users.utils import LoginBlockUtil, MFABlockUtils
|
||||
from users.exceptions import MFANotEnabled
|
||||
from . import errors
|
||||
from .utils import rsa_decrypt, gen_key_pair
|
||||
from .signals import post_auth_success, post_auth_failed
|
||||
@@ -208,18 +209,20 @@ class AuthMixin(PasswordEncryptionViewMixin):
|
||||
ip = self.get_request_ip()
|
||||
self._set_partial_credential_error(username=username, ip=ip, request=request)
|
||||
|
||||
password = password + challenge.strip()
|
||||
if decrypt_passwd:
|
||||
password = self.get_decrypted_password()
|
||||
password = password + challenge.strip()
|
||||
return username, password, public_key, ip, auto_login
|
||||
|
||||
def _check_only_allow_exists_user_auth(self, username):
|
||||
# 仅允许预先存在的用户认证
|
||||
if settings.ONLY_ALLOW_EXIST_USER_AUTH:
|
||||
exist = User.objects.filter(username=username).exists()
|
||||
if not exist:
|
||||
logger.error(f"Only allow exist user auth, login failed: {username}")
|
||||
self.raise_credential_error(errors.reason_user_not_exist)
|
||||
if not settings.ONLY_ALLOW_EXIST_USER_AUTH:
|
||||
return
|
||||
|
||||
exist = User.objects.filter(username=username).exists()
|
||||
if not exist:
|
||||
logger.error(f"Only allow exist user auth, login failed: {username}")
|
||||
self.raise_credential_error(errors.reason_user_not_exist)
|
||||
|
||||
def _check_auth_user_is_valid(self, username, password, public_key):
|
||||
user = authenticate(self.request, username=username, password=password, public_key=public_key)
|
||||
@@ -231,6 +234,17 @@ class AuthMixin(PasswordEncryptionViewMixin):
|
||||
self.raise_credential_error(errors.reason_user_inactive)
|
||||
return user
|
||||
|
||||
def _check_login_mfa_login_if_need(self, user):
|
||||
request = self.request
|
||||
if hasattr(request, 'data'):
|
||||
data = request.data
|
||||
else:
|
||||
data = request.POST
|
||||
code = data.get('code')
|
||||
mfa_type = data.get('mfa_type')
|
||||
if settings.SECURITY_MFA_IN_LOGIN_PAGE and code and mfa_type:
|
||||
self.check_user_mfa(code, mfa_type, user=user)
|
||||
|
||||
def _check_login_acl(self, user, ip):
|
||||
# ACL 限制用户登录
|
||||
from acls.models import LoginACL
|
||||
@@ -255,8 +269,7 @@ class AuthMixin(PasswordEncryptionViewMixin):
|
||||
|
||||
def check_user_auth(self, decrypt_passwd=False):
|
||||
self.check_is_block()
|
||||
request = self.request
|
||||
username, password, public_key, ip, auto_login = self.get_auth_data(decrypt_passwd=decrypt_passwd)
|
||||
username, password, public_key, ip, auto_login = self.get_auth_data(decrypt_passwd)
|
||||
|
||||
self._check_only_allow_exists_user_auth(username)
|
||||
user = self._check_auth_user_is_valid(username, password, public_key)
|
||||
@@ -266,7 +279,11 @@ class AuthMixin(PasswordEncryptionViewMixin):
|
||||
self._check_passwd_is_too_simple(user, password)
|
||||
self._check_passwd_need_update(user)
|
||||
|
||||
# 校验login-mfa, 如果登录页面上显示 mfa 的话
|
||||
self._check_login_mfa_login_if_need(user)
|
||||
|
||||
LoginBlockUtil(username, ip).clean_failed_count()
|
||||
request = self.request
|
||||
request.session['auth_password'] = 1
|
||||
request.session['user_id'] = str(user.id)
|
||||
request.session['auto_login'] = auto_login
|
||||
@@ -348,12 +365,11 @@ class AuthMixin(PasswordEncryptionViewMixin):
|
||||
def check_user_mfa_if_need(self, user):
|
||||
if self.request.session.get('auth_mfa'):
|
||||
return
|
||||
|
||||
if settings.OTP_IN_RADIUS:
|
||||
return
|
||||
|
||||
if not user.mfa_enabled:
|
||||
return
|
||||
|
||||
unset, url = user.mfa_enabled_but_not_set()
|
||||
if unset:
|
||||
raise errors.MFAUnsetError(user, self.request, url)
|
||||
@@ -372,19 +388,29 @@ class AuthMixin(PasswordEncryptionViewMixin):
|
||||
self.request.session['auth_mfa_type'] = ''
|
||||
|
||||
def check_mfa_is_block(self, username, ip, raise_exception=True):
|
||||
if MFABlockUtils(username, ip).is_block():
|
||||
logger.warn('Ip was blocked' + ': ' + username + ':' + ip)
|
||||
exception = errors.BlockMFAError(username=username, request=self.request, ip=ip)
|
||||
if raise_exception:
|
||||
raise exception
|
||||
else:
|
||||
return exception
|
||||
blocked = MFABlockUtils(username, ip).is_block()
|
||||
if not blocked:
|
||||
return
|
||||
logger.warn('Ip was blocked' + ': ' + username + ':' + ip)
|
||||
exception = errors.BlockMFAError(username=username, request=self.request, ip=ip)
|
||||
if raise_exception:
|
||||
raise exception
|
||||
else:
|
||||
return exception
|
||||
|
||||
def check_user_mfa(self, code, mfa_type=MFAType.OTP, user=None):
|
||||
user = user if user else self.get_user_from_session()
|
||||
if not user.mfa_enabled:
|
||||
return True
|
||||
|
||||
if not (bool(user.otp_secret_key) and mfa_type == MFAType.OTP):
|
||||
self.set_passwd_verify_on_session(user)
|
||||
raise errors.OTPRequiredError(reverse_lazy('authentication:user-otp-enable-bind'))
|
||||
|
||||
def check_user_mfa(self, code, mfa_type=MFAType.OTP):
|
||||
user = self.get_user_from_session()
|
||||
ip = self.get_request_ip()
|
||||
self.check_mfa_is_block(user.username, ip)
|
||||
ok = user.check_mfa(code, mfa_type=mfa_type)
|
||||
|
||||
if ok:
|
||||
self.mark_mfa_ok()
|
||||
return
|
||||
@@ -468,3 +494,31 @@ class AuthMixin(PasswordEncryptionViewMixin):
|
||||
if args:
|
||||
guard_url = "%s?%s" % (guard_url, args)
|
||||
return redirect(guard_url)
|
||||
|
||||
@staticmethod
|
||||
def get_user_mfa_methods(user=None):
|
||||
otp_enabled = user.otp_secret_key if user else True
|
||||
# 没有用户时,或者有用户并且有电话配置
|
||||
sms_enabled = any([user and user.phone, not user]) \
|
||||
and settings.SMS_ENABLED and settings.XPACK_ENABLED
|
||||
|
||||
methods = [
|
||||
{
|
||||
'name': 'otp',
|
||||
'label': 'MFA',
|
||||
'enable': otp_enabled,
|
||||
'selected': False,
|
||||
},
|
||||
{
|
||||
'name': 'sms',
|
||||
'label': _('SMS'),
|
||||
'enable': sms_enabled,
|
||||
'selected': False,
|
||||
},
|
||||
]
|
||||
|
||||
for item in methods:
|
||||
if item['enable']:
|
||||
item['selected'] = True
|
||||
break
|
||||
return methods
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import uuid
|
||||
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import ugettext_lazy as _, ugettext as __
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from rest_framework.authtoken.models import Token
|
||||
from django.conf import settings
|
||||
|
||||
|
@@ -1,10 +1,8 @@
|
||||
import random
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.cache import cache
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from common.message.backends.sms.alibaba import AlibabaSMS
|
||||
from common.message.backends.sms import SMS
|
||||
from common.utils import get_logger
|
||||
from common.exceptions import JMSException
|
||||
|
@@ -5,9 +5,9 @@
|
||||
<div class="col-sm-6">
|
||||
<div class="input-group-prepend">
|
||||
{% if audio %}
|
||||
<a title="{% trans "Play CAPTCHA as audio file" %}" href="{{ audio }}">
|
||||
<a title="{% trans "Play CAPTCHA as audio file" %}" href="{{ audio }}"></a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% include "django/forms/widgets/multiwidget.html" %}
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -10,23 +10,15 @@
|
||||
{{ JMS_TITLE }}
|
||||
</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
{% include '_head_css_js.html' %}
|
||||
<!-- Stylesheets -->
|
||||
<link href="{% static 'css/bootstrap.min.css' %}" rel="stylesheet">
|
||||
<link href="{% static 'css/font-awesome.min.css' %}" rel="stylesheet">
|
||||
<link href="{% static 'css/bootstrap-style.css' %}" rel="stylesheet">
|
||||
<link href="{% static 'css/login-style.css' %}" rel="stylesheet">
|
||||
<link href="{% static 'css/style.css' %}" rel="stylesheet">
|
||||
<link href="{% static 'css/jumpserver.css' %}" rel="stylesheet">
|
||||
|
||||
<!-- scripts -->
|
||||
<script src="{% static 'js/jquery-3.1.1.min.js' %}"></script>
|
||||
<script src="{% static 'js/plugins/sweetalert/sweetalert.min.js' %}"></script>
|
||||
<script src="{% static 'js/bootstrap.min.js' %}"></script>
|
||||
<script src="{% static 'js/plugins/datatables/datatables.min.js' %}"></script>
|
||||
<script src="{% static "js/jumpserver.js" %}"></script>
|
||||
|
||||
<style>
|
||||
.login-content {
|
||||
box-shadow: 0 5px 5px -3px rgb(0 0 0 / 20%), 0 8px 10px 1px rgb(0 0 0 / 14%), 0 3px 14px 2px rgb(0 0 0 / 12%);
|
||||
box-shadow: 0 5px 5px -3px rgba(0, 0, 0, 0.15), 0 8px 10px 1px rgba(0, 0, 0, 0.14), 0 3px 14px 2px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
.help-block {
|
||||
@@ -49,17 +41,22 @@
|
||||
}
|
||||
|
||||
.login-content {
|
||||
height: 472px;
|
||||
width: 984px;
|
||||
height: 490px;
|
||||
width: 1066px;
|
||||
margin-right: auto;
|
||||
margin-left: auto;
|
||||
margin-top: calc((100vh - 470px) / 3);
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: #f2f2f2;
|
||||
height: calc(100vh - (100vh - 470px) / 3);
|
||||
}
|
||||
|
||||
.captcha {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.right-image-box {
|
||||
height: 100%;
|
||||
width: 50%;
|
||||
@@ -73,18 +70,10 @@
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.captcha {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.red-fonts {
|
||||
color: red;
|
||||
}
|
||||
|
||||
.field-error {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.form-group.has-error {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
@@ -109,14 +98,6 @@
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
.radio, .checkbox {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#github_star {
|
||||
float: right;
|
||||
margin: 10px 10px 0 0;
|
||||
}
|
||||
.more-login-item {
|
||||
border-right: 1px dashed #dedede;
|
||||
padding-left: 5px;
|
||||
@@ -127,11 +108,18 @@
|
||||
border: none;
|
||||
}
|
||||
|
||||
.select-con {
|
||||
width: 22%;
|
||||
}
|
||||
|
||||
.mfa-div {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="login-content ">
|
||||
<div class="login-content">
|
||||
<div class="right-image-box">
|
||||
<a href="{% if not XPACK_ENABLED %}https://github.com/jumpserver/jumpserver{% endif %}">
|
||||
<img src="{{ LOGIN_IMAGE_URL }}" style="height: 100%; width: 100%"/>
|
||||
@@ -170,6 +158,10 @@
|
||||
</div>
|
||||
{% if form.challenge %}
|
||||
{% bootstrap_field form.challenge show_label=False %}
|
||||
{% elif form.mfa_type %}
|
||||
<div class="form-group" style="display: flex">
|
||||
{% include '_mfa_otp_login.html' %}
|
||||
</div>
|
||||
{% elif form.captcha %}
|
||||
<div class="captch-field">
|
||||
{% bootstrap_field form.captcha show_label=False %}
|
||||
@@ -233,7 +225,7 @@
|
||||
var password = $('#password').val(); //明文密码
|
||||
var passwordEncrypted = encryptLoginPassword(password, rsaPublicKey)
|
||||
$('#password-hidden').val(passwordEncrypted); //返回给密码输入input
|
||||
$('#login-form').submit();//post提交
|
||||
$('#login-form').submit(); //post提交
|
||||
}
|
||||
|
||||
$(document).ready(function () {
|
||||
|
@@ -13,78 +13,19 @@
|
||||
<p class="red-fonts">{{ form.code.errors.as_text }}</p>
|
||||
{% endif %}
|
||||
<div class="form-group">
|
||||
<select id="verify-method-select" name="mfa_type" class="form-control" onchange="select_change(this.value)">
|
||||
{% for method in methods %}
|
||||
<option value="{{ method.name }}" {% if method.selected %} selected {% endif %} {% if not method.enable %} disabled {% endif %}>{{ method.label }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
{% include '_mfa_otp_login.html' %}
|
||||
</div>
|
||||
<div class="form-group" style="display: flex">
|
||||
|
||||
<input id="mfa-code" required type="text" class="form-control" name="code" placeholder="{% trans 'Please enter the verification code' %}" autofocus="autofocus">
|
||||
<button id='send-sms-verify-code' type="button" class="btn btn-info full-width m-b" onclick="sendSMSVerifyCode()" style="width: 150px!important;">{% trans 'Send verification code' %}</button>
|
||||
|
||||
</div>
|
||||
|
||||
<button id='submit_button' type="submit" class="btn btn-primary block full-width m-b">{% trans 'Next' %}</button>
|
||||
<button id='submit_button' type="submit"
|
||||
class="btn btn-primary block full-width m-b">{% trans 'Next' %}</button>
|
||||
<div>
|
||||
<small>{% trans "Can't provide security? Please contact the administrator!" %}</small>
|
||||
</div>
|
||||
</form>
|
||||
<style type="text/css">
|
||||
.disabledBtn {
|
||||
background: #e6e4e4!important;
|
||||
border-color: #d8d5d5!important;
|
||||
color: #949191!important;
|
||||
}
|
||||
.mfa-div {
|
||||
margin-top: 15px;
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
|
||||
var methodSelect = document.getElementById('verify-method-select');
|
||||
if (methodSelect.value !== null) {
|
||||
select_change(methodSelect.value);
|
||||
}
|
||||
|
||||
function select_change(type){
|
||||
var currentBtn = document.getElementById('send-sms-verify-code');
|
||||
|
||||
if (type == "sms") {
|
||||
currentBtn.style.display = "block";
|
||||
currentBtn.disabled = false;
|
||||
}
|
||||
else {
|
||||
currentBtn.style.display = "none";
|
||||
currentBtn.disabled = true;
|
||||
}
|
||||
}
|
||||
function sendSMSVerifyCode(){
|
||||
var currentBtn = document.getElementById('send-sms-verify-code');
|
||||
var time = 60
|
||||
var url = "{% url 'api-auth:sms-verify-code-send' %}";
|
||||
requestApi({
|
||||
url: url,
|
||||
method: "POST",
|
||||
success: function (data) {
|
||||
currentBtn.innerHTML = `{% trans 'Wait: ' %} ${time}`;
|
||||
currentBtn.disabled = true
|
||||
currentBtn.classList.add("disabledBtn" )
|
||||
var TimeInterval = setInterval(()=>{
|
||||
--time
|
||||
currentBtn.innerHTML = `{% trans 'Wait: ' %} ${time}`;
|
||||
if(time === 0) {
|
||||
currentBtn.innerHTML = "{% trans 'Send verification code' %}"
|
||||
currentBtn.disabled = false
|
||||
currentBtn.classList.remove("disabledBtn")
|
||||
clearInterval(TimeInterval)
|
||||
}
|
||||
},1000)
|
||||
alert("{% trans 'The verification code has been sent' %}");
|
||||
},
|
||||
error: function (text, data) {
|
||||
alert(data.detail)
|
||||
},
|
||||
flash_message: false
|
||||
})
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
@@ -13,7 +13,6 @@ router.register('connection-token', api.UserConnectionTokenViewSet, 'connection-
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
# path('token/', api.UserToken.as_view(), name='user-token'),
|
||||
path('wecom/qr/unbind/', api.WeComQRUnBindForUserApi.as_view(), name='wecom-qr-unbind'),
|
||||
path('wecom/qr/unbind/<uuid:user_id>/', api.WeComQRUnBindForAdminApi.as_view(), name='wecom-qr-unbind-for-admin'),
|
||||
|
||||
|
@@ -29,7 +29,6 @@ from ..const import RSA_PRIVATE_KEY, RSA_PUBLIC_KEY
|
||||
from .. import mixins, errors
|
||||
from ..forms import get_user_login_form_cls
|
||||
|
||||
|
||||
__all__ = [
|
||||
'UserLoginView', 'UserLogoutView',
|
||||
'UserLoginGuardView', 'UserLoginWaitConfirmView',
|
||||
@@ -117,7 +116,6 @@ class UserLoginView(mixins.AuthMixin, FormView):
|
||||
except errors.AuthFailedError as e:
|
||||
form.add_error(None, e.msg)
|
||||
self.set_login_failed_mark()
|
||||
|
||||
form_cls = get_user_login_form_cls(captcha=True)
|
||||
new_form = form_cls(data=form.data)
|
||||
new_form._errors = form.errors
|
||||
@@ -125,11 +123,20 @@ class UserLoginView(mixins.AuthMixin, FormView):
|
||||
self.request.session.set_test_cookie()
|
||||
return self.render_to_response(context)
|
||||
except (
|
||||
errors.PasswdTooSimple,
|
||||
errors.PasswordRequireResetError,
|
||||
errors.PasswdNeedUpdate
|
||||
errors.PasswdTooSimple,
|
||||
errors.PasswordRequireResetError,
|
||||
errors.PasswdNeedUpdate
|
||||
) as e:
|
||||
return redirect(e.url)
|
||||
except (
|
||||
errors.MFAUnsetError,
|
||||
errors.MFAFailedError,
|
||||
errors.BlockMFAError
|
||||
) as e:
|
||||
form.add_error('code', e.msg)
|
||||
return super().form_invalid(form)
|
||||
except errors.OTPRequiredError as e:
|
||||
return redirect(e.url)
|
||||
self.clear_rsa_key()
|
||||
return self.redirect_to_guard_view()
|
||||
|
||||
@@ -150,31 +157,31 @@ class UserLoginView(mixins.AuthMixin, FormView):
|
||||
'name': 'OpenID',
|
||||
'enabled': settings.AUTH_OPENID,
|
||||
'url': reverse('authentication:openid:login'),
|
||||
'logo': static('img/login_oidc_logo.png')
|
||||
'logo': static('img/login_oidc_logo.png')
|
||||
},
|
||||
{
|
||||
'name': 'CAS',
|
||||
'enabled': settings.AUTH_CAS,
|
||||
'url': reverse('authentication:cas:cas-login'),
|
||||
'logo': static('img/login_cas_logo.png')
|
||||
'logo': static('img/login_cas_logo.png')
|
||||
},
|
||||
{
|
||||
'name': _('WeCom'),
|
||||
'enabled': settings.AUTH_WECOM,
|
||||
'url': reverse('authentication:wecom-qr-login'),
|
||||
'logo': static('img/login_wecom_logo.png')
|
||||
'logo': static('img/login_wecom_logo.png')
|
||||
},
|
||||
{
|
||||
'name': _('DingTalk'),
|
||||
'enabled': settings.AUTH_DINGTALK,
|
||||
'url': reverse('authentication:dingtalk-qr-login'),
|
||||
'logo': static('img/login_dingtalk_logo.png')
|
||||
'logo': static('img/login_dingtalk_logo.png')
|
||||
},
|
||||
{
|
||||
'name': _('FeiShu'),
|
||||
'enabled': settings.AUTH_FEISHU,
|
||||
'url': reverse('authentication:feishu-qr-login'),
|
||||
'logo': static('img/login_feishu_logo.png')
|
||||
'logo': static('img/login_feishu_logo.png')
|
||||
}
|
||||
]
|
||||
return [method for method in auth_methods if method['enabled']]
|
||||
@@ -191,7 +198,8 @@ class UserLoginView(mixins.AuthMixin, FormView):
|
||||
context = {
|
||||
'demo_mode': os.environ.get("DEMO_MODE"),
|
||||
'auth_methods': self.get_support_auth_methods(),
|
||||
'forgot_password_url': self.get_forgot_password_url()
|
||||
'forgot_password_url': self.get_forgot_password_url(),
|
||||
'methods': self.get_user_mfa_methods(),
|
||||
}
|
||||
kwargs.update(context)
|
||||
return super().get_context_data(**kwargs)
|
||||
|
@@ -20,11 +20,11 @@ class UserLoginOtpView(mixins.AuthMixin, FormView):
|
||||
redirect_field_name = 'next'
|
||||
|
||||
def form_valid(self, form):
|
||||
otp_code = form.cleaned_data.get('code')
|
||||
code = form.cleaned_data.get('code')
|
||||
mfa_type = form.cleaned_data.get('mfa_type')
|
||||
|
||||
try:
|
||||
self.check_user_mfa(otp_code, mfa_type)
|
||||
self.check_user_mfa(code, mfa_type)
|
||||
return redirect_to_guard_view()
|
||||
except (errors.MFAFailedError, errors.BlockMFAError) as e:
|
||||
form.add_error('code', e.msg)
|
||||
@@ -37,26 +37,6 @@ class UserLoginOtpView(mixins.AuthMixin, FormView):
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
user = self.get_user_from_session()
|
||||
context = {
|
||||
'methods': [
|
||||
{
|
||||
'name': 'otp',
|
||||
'label': _('One-time password'),
|
||||
'enable': bool(user.otp_secret_key),
|
||||
'selected': False,
|
||||
},
|
||||
{
|
||||
'name': 'sms',
|
||||
'label': _('SMS'),
|
||||
'enable': bool(user.phone) and settings.SMS_ENABLED and settings.XPACK_ENABLED,
|
||||
'selected': False,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
for item in context['methods']:
|
||||
if item['enable']:
|
||||
item['selected'] = True
|
||||
break
|
||||
context.update(kwargs)
|
||||
return context
|
||||
methods = self.get_user_mfa_methods(user)
|
||||
kwargs.update({'methods': methods})
|
||||
return kwargs
|
||||
|
Reference in New Issue
Block a user