diff --git a/apps/authentication/forms.py b/apps/authentication/forms.py index 84e923a8e..2f03d935b 100644 --- a/apps/authentication/forms.py +++ b/apps/authentication/forms.py @@ -2,6 +2,7 @@ # from django import forms +from django.conf import settings from django.utils.translation import gettext_lazy as _ from captcha.fields import CaptchaField @@ -21,9 +22,24 @@ class UserLoginForm(forms.Form): ) -class UserLoginCaptchaForm(UserLoginForm): +class UserCheckOtpCodeForm(forms.Form): + otp_code = forms.CharField(label=_('MFA code'), max_length=6) + + +class CaptchaMixin(forms.Form): captcha = CaptchaField() -class UserCheckOtpCodeForm(forms.Form): - otp_code = forms.CharField(label=_('MFA code'), max_length=6) +class ChallengeMixin(forms.Form): + challenge = forms.CharField(label=_('MFA code'), max_length=6, + required=False) + + +def get_user_login_form_cls(*, captcha=False): + bases = [] + if settings.SECURITY_LOGIN_CAPTCHA_ENABLED and captcha: + bases.append(CaptchaMixin) + if settings.SECURITY_LOGIN_CHALLENGE_ENABLED: + bases.append(ChallengeMixin) + bases.append(UserLoginForm) + return type('UserLoginForm', tuple(bases), {}) diff --git a/apps/authentication/mixins.py b/apps/authentication/mixins.py index 5b3738c98..cdc7856bd 100644 --- a/apps/authentication/mixins.py +++ b/apps/authentication/mixins.py @@ -1,7 +1,9 @@ # -*- coding: utf-8 -*- # +from functools import partial import time from django.conf import settings +from django.contrib.auth import authenticate from common.utils import get_object_or_none, get_request_ip, get_logger from users.models import User @@ -9,7 +11,7 @@ from users.utils import ( is_block_login, clean_failed_count ) from . import errors -from .utils import check_user_valid +from .utils import rsa_decrypt from .signals import post_auth_success, post_auth_failed logger = get_logger(__name__) @@ -54,21 +56,41 @@ class AuthMixin: self.check_is_block() request = self.request if hasattr(request, 'data'): - username = request.data.get('username', '') - password = request.data.get('password', '') - public_key = request.data.get('public_key', '') + data = request.data else: - username = request.POST.get('username', '') - password = request.POST.get('password', '') - public_key = request.POST.get('public_key', '') - user, error = check_user_valid( - request=request, username=username, password=password, public_key=public_key - ) + data = request.POST + username = data.get('username', '') + password = data.get('password', '') + challenge = data.get('challenge', '') + public_key = data.get('public_key', '') ip = self.get_request_ip() + + CredentialError = partial(errors.CredentialError, username=username, ip=ip, request=request) + + # 获取解密密钥,对密码进行解密 + rsa_private_key = request.session.get('rsa_private_key') + if rsa_private_key is not None: + try: + password = rsa_decrypt(password, rsa_private_key) + except Exception as e: + logger.error(e, exc_info=True) + logger.error('Need decrypt password => {}'.format(password)) + raise CredentialError(error=errors.reason_password_decrypt_failed) + + user = authenticate(request, + username=username, + password=password + challenge.strip(), + public_key=public_key) + if not user: - raise errors.CredentialError( - username=username, error=error, ip=ip, request=request - ) + raise CredentialError(error=errors.reason_password_failed) + elif user.is_expired: + raise CredentialError(error=errors.reason_user_inactive) + elif not user.is_active: + raise CredentialError(error=errors.reason_user_inactive) + elif user.password_has_expired: + raise CredentialError(error=errors.reason_password_expired) + clean_failed_count(username, ip) request.session['auth_password'] = 1 request.session['user_id'] = str(user.id) diff --git a/apps/authentication/templates/authentication/login.html b/apps/authentication/templates/authentication/login.html index 14978e426..a6dec7d9d 100644 --- a/apps/authentication/templates/authentication/login.html +++ b/apps/authentication/templates/authentication/login.html @@ -33,6 +33,16 @@ {% endif %} + {% if form.challenge %} +
+ + {% if form.errors.challenge %} +
+

{{ form.errors.challenge.as_text }}

+
+ {% endif %} +
+ {% endif %}
{{ form.captcha }}
diff --git a/apps/authentication/templates/authentication/xpack_login.html b/apps/authentication/templates/authentication/xpack_login.html index 5929650df..77edc3582 100644 --- a/apps/authentication/templates/authentication/xpack_login.html +++ b/apps/authentication/templates/authentication/xpack_login.html @@ -67,16 +67,16 @@
-
+
{{ JMS_TITLE }}
{% trans 'Welcome back, please enter username and password to login' %}
-
+
-
+
{% csrf_token %} {% if form.non_field_errors %} @@ -105,6 +105,16 @@
{% endif %}
+ {% if form.challenge %} +
+ + {% if form.errors.challenge %} +
+

{{ form.errors.challenge.as_text }}

+
+ {% endif %} +
+ {% endif %}
{{ form.captcha }}
diff --git a/apps/authentication/utils.py b/apps/authentication/utils.py index 359778cb6..cb697c237 100644 --- a/apps/authentication/utils.py +++ b/apps/authentication/utils.py @@ -4,12 +4,9 @@ import base64 from Crypto.PublicKey import RSA from Crypto.Cipher import PKCS1_v1_5 from Crypto import Random -from django.contrib.auth import authenticate from common.utils import get_logger -from . import errors - logger = get_logger(__file__) @@ -41,33 +38,3 @@ def rsa_decrypt(cipher_text, rsa_private_key=None): cipher = PKCS1_v1_5.new(key) message = cipher.decrypt(base64.b64decode(cipher_text.encode()), 'error').decode() return message - - -def check_user_valid(**kwargs): - password = kwargs.pop('password', None) - public_key = kwargs.pop('public_key', None) - username = kwargs.pop('username', None) - request = kwargs.get('request') - - # 获取解密密钥,对密码进行解密 - rsa_private_key = request.session.get('rsa_private_key') - if rsa_private_key is not None: - try: - password = rsa_decrypt(password, rsa_private_key) - except Exception as e: - logger.error(e, exc_info=True) - logger.error('Need decrypt password => {}'.format(password)) - return None, errors.reason_password_decrypt_failed - - user = authenticate(request, username=username, - password=password, public_key=public_key) - if not user: - return None, errors.reason_password_failed - elif user.is_expired: - return None, errors.reason_user_inactive - elif not user.is_active: - return None, errors.reason_user_inactive - elif user.password_has_expired: - return None, errors.reason_password_expired - - return user, '' diff --git a/apps/authentication/views/login.py b/apps/authentication/views/login.py index 7ef72235b..141d0f6e7 100644 --- a/apps/authentication/views/login.py +++ b/apps/authentication/views/login.py @@ -22,7 +22,8 @@ from common.utils import get_request_ip, get_object_or_none from users.utils import ( redirect_user_first_login_or_index ) -from .. import forms, mixins, errors, utils +from .. import mixins, errors, utils +from ..forms import get_user_login_form_cls __all__ = [ @@ -35,8 +36,6 @@ __all__ = [ @method_decorator(csrf_protect, name='dispatch') @method_decorator(never_cache, name='dispatch') class UserLoginView(mixins.AuthMixin, FormView): - form_class = forms.UserLoginForm - form_class_captcha = forms.UserLoginCaptchaForm key_prefix_captcha = "_LOGIN_INVALID_{}" redirect_field_name = 'next' @@ -87,7 +86,8 @@ class UserLoginView(mixins.AuthMixin, FormView): form.add_error(None, e.msg) ip = self.get_request_ip() cache.set(self.key_prefix_captcha.format(ip), 1, 3600) - new_form = self.form_class_captcha(data=form.data) + form_cls = get_user_login_form_cls(captcha=True) + new_form = form_cls(data=form.data) new_form._errors = form.errors context = self.get_context_data(form=new_form) return self.render_to_response(context) @@ -103,9 +103,9 @@ class UserLoginView(mixins.AuthMixin, FormView): def get_form_class(self): ip = get_request_ip(self.request) if cache.get(self.key_prefix_captcha.format(ip)): - return self.form_class_captcha + return get_user_login_form_cls(captcha=True) else: - return self.form_class + return get_user_login_form_cls() def get_context_data(self, **kwargs): # 生成加解密密钥对,public_key传递给前端,private_key存入session中供解密使用 diff --git a/apps/jumpserver/conf.py b/apps/jumpserver/conf.py index 173d134b2..6f521c453 100644 --- a/apps/jumpserver/conf.py +++ b/apps/jumpserver/conf.py @@ -238,6 +238,8 @@ class Config(dict): 'SECURITY_PASSWORD_LOWER_CASE': False, 'SECURITY_PASSWORD_NUMBER': False, 'SECURITY_PASSWORD_SPECIAL_CHAR': False, + 'SECURITY_LOGIN_CHALLENGE_ENABLED': False, + 'SECURITY_LOGIN_CAPTCHA_ENABLED': True, 'HTTP_BIND_HOST': '0.0.0.0', 'HTTP_LISTEN_PORT': 8080, diff --git a/apps/jumpserver/settings/custom.py b/apps/jumpserver/settings/custom.py index 1e552345b..e7d687dc3 100644 --- a/apps/jumpserver/settings/custom.py +++ b/apps/jumpserver/settings/custom.py @@ -41,6 +41,8 @@ SECURITY_PASSWORD_RULES = [ SECURITY_MFA_VERIFY_TTL = CONFIG.SECURITY_MFA_VERIFY_TTL SECURITY_VIEW_AUTH_NEED_MFA = CONFIG.SECURITY_VIEW_AUTH_NEED_MFA SECURITY_SERVICE_ACCOUNT_REGISTRATION = DYNAMIC.SECURITY_SERVICE_ACCOUNT_REGISTRATION +SECURITY_LOGIN_CAPTCHA_ENABLED = CONFIG.SECURITY_LOGIN_CAPTCHA_ENABLED +SECURITY_LOGIN_CHALLENGE_ENABLED = CONFIG.SECURITY_LOGIN_CHALLENGE_ENABLED # Terminal other setting TERMINAL_PASSWORD_AUTH = DYNAMIC.TERMINAL_PASSWORD_AUTH