Commit 12c8cf6b authored by BaiJiangjie's avatar BaiJiangjie

[Update] 添加OTP认证功能

parent 33bc73ab
This diff is collapsed.
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
import uuid import uuid
from django.core.cache import cache from django.core.cache import cache
from django.urls import reverse
from rest_framework import generics from rest_framework import generics
from rest_framework.permissions import AllowAny, IsAuthenticated from rest_framework.permissions import AllowAny, IsAuthenticated
...@@ -139,40 +140,75 @@ class UserProfile(APIView): ...@@ -139,40 +140,75 @@ class UserProfile(APIView):
return Response(self.serializer_class(request.user).data) return Response(self.serializer_class(request.user).data)
class UserAuthApi(APIView): class UserOtpAuthApi(APIView):
permission_classes = (AllowAny,) permission_classes = (AllowAny,)
serializer_class = UserSerializer serializer_class = UserSerializer
def post(self, request): def post(self, request):
otp_check = request.data.get('otp_check', None) otp_code = request.data.get('otp_code', '')
seed = request.data.get('seed', '')
user = cache.get(seed, None)
if not user:
return Response({'msg': '请先进行用户名和密码验证'}, status=401)
if not check_otp_code(user.otp_secret_key, otp_code):
return Response({'msg': 'otp认证失败'}, status=401)
token = generate_token(request, user)
self.write_login_log(request, user)
return Response(
{
'token': token,
'user': self.serializer_class(user).data
}
)
if otp_check: @staticmethod
# otp验证 def write_login_log(request, user):
return self.check_auth_otp(request) login_ip = request.data.get('remote_addr', None)
else: login_type = request.data.get('login_type', '')
# password验证 user_agent = request.data.get('HTTP_USER_AGENT', '')
return self.check_auth_password(request)
if not login_ip:
login_ip = get_login_ip(request)
def check_auth_password(self, request): write_login_log_async.delay(
user.username, ip=login_ip,
type=login_type, user_agent=user_agent,
)
class UserAuthApi(APIView):
permission_classes = (AllowAny,)
serializer_class = UserSerializer
def post(self, request):
user, msg = self.check_user_valid(request) user, msg = self.check_user_valid(request)
if user: if not user:
token = generate_token(request, user)
if not user.otp_enabled:
self.write_login_log(request, user)
return Response({'token': token, 'user': self.serializer_class(user).data})
else:
return Response({'msg': msg}, status=401) return Response({'msg': msg}, status=401)
def check_auth_otp(self, request): if not user.otp_enabled:
otp_code = request.data.get('otp_code', '')
user, msg = self.check_user_valid(request)
if user:
token = generate_token(request, user) token = generate_token(request, user)
if check_otp_code(user.otp_secret_key, otp_code): self.write_login_log(request, user)
self.write_login_log(request, user) return Response(
return Response({'token': token, 'user': self.serializer_class(user).data}) {
return Response({'msg': msg}, status=401) 'token': token,
'user': self.serializer_class(user).data
}
)
seed = uuid.uuid4().hex
cache.set(seed, user, 300)
return Response(
{
'code': 101,
'msg': '请携带seed值,进行OTP二次认证',
'otp_url': reverse('api-users:user-otp-auth'),
'seed': seed,
'user': self.serializer_class(user).data
}, status=300)
@staticmethod @staticmethod
def check_user_valid(request): def check_user_valid(request):
......
...@@ -232,7 +232,7 @@ class User(AbstractUser): ...@@ -232,7 +232,7 @@ class User(AbstractUser):
def disable_otp(self): def disable_otp(self):
self.otp_level = 0 self.otp_level = 0
self.otp_secret_key = '' self.otp_secret_key = None
def to_json(self): def to_json(self):
return OrderedDict({ return OrderedDict({
......
...@@ -21,7 +21,7 @@ class UserSerializer(BulkSerializerMixin, serializers.ModelSerializer): ...@@ -21,7 +21,7 @@ class UserSerializer(BulkSerializerMixin, serializers.ModelSerializer):
list_serializer_class = BulkListSerializer list_serializer_class = BulkListSerializer
exclude = [ exclude = [
'first_name', 'last_name', 'password', '_private_key', 'first_name', 'last_name', 'password', '_private_key',
'_public_key', 'otp_secret_key', 'user_permissions' '_public_key', '_otp_secret_key', 'user_permissions'
] ]
def get_field_names(self, declared_fields, info): def get_field_names(self, declared_fields, info):
......
...@@ -25,7 +25,7 @@ ...@@ -25,7 +25,7 @@
<div> <div>
<a href="{% url 'index' %}">首页</a> <a href="{% url 'index' %}">首页</a>
<b></b> <b></b>
<a href="#">帮助中心</a> <a href="http://docs.jumpserver.org/zh/docs/">文档</a>
<b></b> <b></b>
<a href="https://www.github.com/jumpserver/">GitHub</a> <a href="https://www.github.com/jumpserver/">GitHub</a>
</div> </div>
......
...@@ -66,7 +66,7 @@ ...@@ -66,7 +66,7 @@
<button type="submit" class="btn btn-primary block full-width m-b">{% trans 'Next' %}</button> <button type="submit" class="btn btn-primary block full-width m-b">{% trans 'Next' %}</button>
<a href="#"> <a href="#">
<small>{% trans "Can't provide security? Please contact the administrator" %}</small> <small>{% trans "Can't provide otp code? Please contact the administrator" %}</small>
</a> </a>
</form> </form>
......
...@@ -7,10 +7,8 @@ ...@@ -7,10 +7,8 @@
<div class="verify"> <div class="verify">
<p style="margin:20px auto;"><strong style="color: #000000">使用手机 Google Authenticator 应用扫描以下二维码,获取6位验证码</strong></p> <p style="margin:20px auto;"><strong style="color: #000000">使用手机 Google Authenticator 应用扫描以下二维码,获取6位验证码</strong></p>
<div id="qr_code"></div> <div id="qr_code"></div>
<form class="" role="form" method="post" action=""> <form class="" role="form" method="post" action="">
{% csrf_token %} {% csrf_token %}
...@@ -18,12 +16,13 @@ ...@@ -18,12 +16,13 @@
<input type="text" class="" name="otp_code" placeholder="{% trans 'Six figures' %}" required=""> <input type="text" class="" name="otp_code" placeholder="{% trans 'Six figures' %}" required="">
</div> </div>
<button type="submit" class="next">{% trans 'Next' %}</button>
{% if 'otp_code' in form.errors %} {% if 'otp_code' in form.errors %}
<p style="color: #ed5565">{{ form.otp_code.errors.as_text }}</p> <p style="color: #ed5565">{{ form.otp_code.errors.as_text }}</p>
{% endif %} {% endif %}
<button type="submit" class="next">{% trans 'Next' %}</button>
</form> </form>
</div> </div>
......
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
{% block content %} {% block content %}
<form class="" role="form" method="post" action=""> <form class="" role="form" method="post" action="">
{% csrf_token %} {% csrf_token %}
<div class="form-input"> <div class="form-input">
<input type="text" class="" name="{{ form.username.html_name }}" value="{{ form.username.value }}" readonly="readonly" required=""> <input type="text" class="" name="{{ form.username.html_name }}" value="{{ form.username.value }}" readonly="readonly" required="">
</div> </div>
...@@ -13,13 +14,12 @@ ...@@ -13,13 +14,12 @@
<input type="password" class="" name="{{ form.password.html_name }}" placeholder="{% trans 'Password' %}" required=""> <input type="password" class="" name="{{ form.password.html_name }}" placeholder="{% trans 'Password' %}" required="">
</div> </div>
<button type="submit" class="next">{% trans 'Next' %}</button>
{% if 'password' in form.errors %} {% if 'password' in form.errors %}
<p class="red-fonts">{{ form.password.errors.as_text }}</p> <p class="red-fonts">{{ form.password.errors.as_text }}</p>
{% endif %} {% endif %}
<button type="submit" class="next">{% trans 'Next' %}</button>
</form> </form>
{% endblock %} {% endblock %}
...@@ -152,8 +152,7 @@ ...@@ -152,8 +152,7 @@
href=" href="
{% if request.user.otp_enabled and request.user.otp_secret_key %} {% if request.user.otp_enabled and request.user.otp_secret_key %}
{% if request.user.otp_force_enabled %} {% if request.user.otp_force_enabled %}
javascript:void(0) " disabled >{% trans 'Disable' %}
"><span style="color:#ed5565">{% trans 'Disable' %}</span>
{% else %} {% else %}
{% url 'users:user-otp-disable-authentication' %} {% url 'users:user-otp-disable-authentication' %}
">{% trans 'Disable' %} ">{% trans 'Disable' %}
......
...@@ -20,6 +20,7 @@ urlpatterns = [ ...@@ -20,6 +20,7 @@ urlpatterns = [
url(r'^v1/connection-token/$', api.UserConnectionTokenApi.as_view(), name='connection-token'), url(r'^v1/connection-token/$', api.UserConnectionTokenApi.as_view(), name='connection-token'),
url(r'^v1/profile/$', api.UserProfile.as_view(), name='user-profile'), url(r'^v1/profile/$', api.UserProfile.as_view(), name='user-profile'),
url(r'^v1/auth/$', api.UserAuthApi.as_view(), name='user-auth'), url(r'^v1/auth/$', api.UserAuthApi.as_view(), name='user-auth'),
url(r'^v1/otp/auth/$', api.UserOtpAuthApi.as_view(), name='user-otp-auth'),
url(r'^v1/users/(?P<pk>[0-9a-zA-Z\-]{36})/password/$', url(r'^v1/users/(?P<pk>[0-9a-zA-Z\-]{36})/password/$',
api.ChangeUserPasswordApi.as_view(), name='change-user-password'), api.ChangeUserPasswordApi.as_view(), name='change-user-password'),
url(r'^v1/users/(?P<pk>[0-9a-zA-Z\-]{36})/password/reset/$', url(r'^v1/users/(?P<pk>[0-9a-zA-Z\-]{36})/password/reset/$',
......
...@@ -224,16 +224,14 @@ def get_ip_city(ip, timeout=10): ...@@ -224,16 +224,14 @@ def get_ip_city(ip, timeout=10):
return city return city
def get_user(request): def get_tmp_user_from_session(request):
if is_login(request): user_id = request.session.get('tmp_user_id')
user = request.user user = get_object_or_none(User, pk=user_id)
else:
user = cache.get(request.session.session_key)
return user return user
def is_login(request): def set_tmp_user_to_session(request, user):
return isinstance(request.user, User) request.session['tmp_user_id'] = str(user.id)
def redirect_user_first_login_or_index(request, redirect_field_name): def redirect_user_first_login_or_index(request, redirect_field_name):
...@@ -244,9 +242,15 @@ def redirect_user_first_login_or_index(request, redirect_field_name): ...@@ -244,9 +242,15 @@ def redirect_user_first_login_or_index(request, redirect_field_name):
request.GET.get(redirect_field_name, reverse('index'))) request.GET.get(redirect_field_name, reverse('index')))
def generate_otp_uri(user, issuer="Jumpserver"): def generate_otp_uri(request, issuer="Jumpserver"):
otp_secret_key = base64.b32encode(os.urandom(10)).decode('utf-8') if request.user.is_authenticated:
cache.set('otp_secret_key', otp_secret_key, 300) user = request.user
else:
user = get_tmp_user_from_session(request)
otp_secret_key = cache.get(request.session.session_key+'otp_key', '')
if not otp_secret_key:
otp_secret_key = base64.b32encode(os.urandom(10)).decode('utf-8')
cache.set(request.session.session_key+'otp_key', otp_secret_key, 600)
totp = pyotp.TOTP(otp_secret_key) totp = pyotp.TOTP(otp_secret_key)
return totp.provisioning_uri(name=user.username, issuer_name=issuer) return totp.provisioning_uri(name=user.username, issuer_name=issuer)
......
...@@ -19,12 +19,12 @@ from django.views.generic.base import TemplateView ...@@ -19,12 +19,12 @@ from django.views.generic.base import TemplateView
from django.views.generic.edit import FormView from django.views.generic.edit import FormView
from formtools.wizard.views import SessionWizardView from formtools.wizard.views import SessionWizardView
from django.conf import settings from django.conf import settings
from django.core.cache import cache
from common.utils import get_object_or_none from common.utils import get_object_or_none
from common.mixins import DatetimeSearchMixin, AdminUserRequiredMixin from common.mixins import DatetimeSearchMixin, AdminUserRequiredMixin
from ..models import User, LoginLog from ..models import User, LoginLog
from ..utils import send_reset_password_mail, check_otp_code , get_login_ip, redirect_user_first_login_or_index from ..utils import send_reset_password_mail, check_otp_code, get_login_ip, redirect_user_first_login_or_index, \
get_tmp_user_from_session, set_tmp_user_to_session
from ..tasks import write_login_log_async from ..tasks import write_login_log_async
from .. import forms from .. import forms
...@@ -54,11 +54,12 @@ class UserLoginView(FormView): ...@@ -54,11 +54,12 @@ class UserLoginView(FormView):
def form_valid(self, form): def form_valid(self, form):
if not self.request.session.test_cookie_worked(): if not self.request.session.test_cookie_worked():
return HttpResponse(_("Please enable cookies and try again.")) return HttpResponse(_("Please enable cookies and try again."))
cache.set(self.request.session.session_key, form.get_user(), 600)
set_tmp_user_to_session(self.request, form.get_user())
return redirect(self.get_success_url()) return redirect(self.get_success_url())
def get_success_url(self): def get_success_url(self):
user = cache.get(self.request.session.session_key) user = get_tmp_user_from_session(self.request)
if user.otp_enabled and user.otp_secret_key: if user.otp_enabled and user.otp_secret_key:
# 1,2 & T # 1,2 & T
...@@ -94,7 +95,7 @@ class UserLoginOtpView(FormView): ...@@ -94,7 +95,7 @@ class UserLoginOtpView(FormView):
redirect_field_name = 'next' redirect_field_name = 'next'
def form_valid(self, form): def form_valid(self, form):
user = cache.get(self.request.session.session_key) user = get_tmp_user_from_session(self.request)
otp_code = form.cleaned_data.get('otp_code') otp_code = form.cleaned_data.get('otp_code')
otp_secret_key = user.otp_secret_key otp_secret_key = user.otp_secret_key
......
...@@ -35,7 +35,7 @@ from common.mixins import JSONResponseMixin ...@@ -35,7 +35,7 @@ from common.mixins import JSONResponseMixin
from common.utils import get_logger, get_object_or_none, is_uuid, ssh_key_gen from common.utils import get_logger, get_object_or_none, is_uuid, ssh_key_gen
from .. import forms from .. import forms
from ..models import User, UserGroup from ..models import User, UserGroup
from ..utils import AdminUserRequiredMixin, generate_otp_uri, check_otp_code, get_user, is_login from ..utils import AdminUserRequiredMixin, generate_otp_uri, check_otp_code, get_tmp_user_from_session
from ..signals import post_user_create from ..signals import post_user_create
from ..tasks import write_login_log_async from ..tasks import write_login_log_async
...@@ -400,20 +400,31 @@ class UserOtpEnableAuthenticationView(FormView): ...@@ -400,20 +400,31 @@ class UserOtpEnableAuthenticationView(FormView):
form_class = forms.UserCheckPasswordForm form_class = forms.UserCheckPasswordForm
def get_form(self, form_class=None): def get_form(self, form_class=None):
if self.request.user.is_authenticated:
user = self.request.user
else:
user = get_tmp_user_from_session(self.request)
form = super().get_form(form_class=form_class) form = super().get_form(form_class=form_class)
form['username'].initial = get_user(self.request).username form['username'].initial = user.username
return form return form
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
if self.request.user.is_authenticated:
user = self.request.user
else:
user = get_tmp_user_from_session(self.request)
context = { context = {
'user': get_user(self.request) 'user': user
} }
kwargs.update(context) kwargs.update(context)
return super().get_context_data(**kwargs) return super().get_context_data(**kwargs)
def form_valid(self, form): def form_valid(self, form):
if self.request.user.is_authenticated:
user = self.request.user
else:
user = get_tmp_user_from_session(self.request)
password = form.cleaned_data.get('password') password = form.cleaned_data.get('password')
user = get_user(self.request)
user = authenticate(username=user.username, password=password) user = authenticate(username=user.username, password=password)
if not user: if not user:
form.add_error("password", _("Password invalid")) form.add_error("password", _("Password invalid"))
...@@ -428,8 +439,12 @@ class UserOtpEnableInstallAppView(TemplateView): ...@@ -428,8 +439,12 @@ class UserOtpEnableInstallAppView(TemplateView):
template_name = 'users/user_otp_enable_install_app.html' template_name = 'users/user_otp_enable_install_app.html'
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
if self.request.user.is_authenticated:
user = self.request.user
else:
user = get_tmp_user_from_session(self.request)
context = { context = {
'user': get_user(self.request) 'user': user
} }
kwargs.update(context) kwargs.update(context)
return super().get_context_data(**kwargs) return super().get_context_data(**kwargs)
...@@ -441,16 +456,20 @@ class UserOtpEnableBindView(TemplateView, FormView): ...@@ -441,16 +456,20 @@ class UserOtpEnableBindView(TemplateView, FormView):
success_url = reverse_lazy('users:user-otp-settings-success') success_url = reverse_lazy('users:user-otp-settings-success')
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
if self.request.user.is_authenticated:
user = self.request.user
else:
user = get_tmp_user_from_session(self.request)
context = { context = {
'otp_uri': generate_otp_uri(user=get_user(self.request)), 'otp_uri': generate_otp_uri(self.request),
'user': get_user(self.request) 'user': user
} }
kwargs.update(context) kwargs.update(context)
return super().get_context_data(**kwargs) return super().get_context_data(**kwargs)
def form_valid(self, form): def form_valid(self, form):
otp_code = form.cleaned_data.get('otp_code') otp_code = form.cleaned_data.get('otp_code')
otp_secret_key = cache.get('otp_secret_key') otp_secret_key = cache.get(self.request.session.session_key+'otp_key', '')
if check_otp_code(otp_secret_key, otp_code): if check_otp_code(otp_secret_key, otp_code):
self.save_otp(otp_secret_key) self.save_otp(otp_secret_key)
...@@ -461,7 +480,10 @@ class UserOtpEnableBindView(TemplateView, FormView): ...@@ -461,7 +480,10 @@ class UserOtpEnableBindView(TemplateView, FormView):
return self.form_invalid(form) return self.form_invalid(form)
def save_otp(self, otp_secret_key): def save_otp(self, otp_secret_key):
user = get_user(self.request) if self.request.user.is_authenticated:
user = self.request.user
else:
user = get_tmp_user_from_session(self.request)
user.enable_otp() user.enable_otp()
user.otp_secret_key = otp_secret_key user.otp_secret_key = otp_secret_key
user.save() user.save()
...@@ -489,11 +511,8 @@ class UserOtpDisableAuthenticationView(FormView): ...@@ -489,11 +511,8 @@ class UserOtpDisableAuthenticationView(FormView):
class UserOtpSettingsSuccessView(TemplateView): class UserOtpSettingsSuccessView(TemplateView):
template_name = 'flash_message_standalone.html' template_name = 'flash_message_standalone.html'
def get(self, request, *args, **kwargs): # def get(self, request, *args, **kwargs):
response = super().get(request, *args, **kwargs) # return super().get(request, *args, **kwargs)
if is_login(request):
auth_logout(request)
return response
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
title, describe = self.get_title_describe() title, describe = self.get_title_describe()
...@@ -508,7 +527,11 @@ class UserOtpSettingsSuccessView(TemplateView): ...@@ -508,7 +527,11 @@ class UserOtpSettingsSuccessView(TemplateView):
return super().get_context_data(**kwargs) return super().get_context_data(**kwargs)
def get_title_describe(self): def get_title_describe(self):
user = get_user(self.request) if self.request.user.is_authenticated:
user = self.request.user
auth_logout(self.request)
else:
user = get_tmp_user_from_session(self.request)
title = _('OTP enable success') title = _('OTP enable success')
describe = _('OTP enable success, return login page') describe = _('OTP enable success, return login page')
if not user.otp_enabled: if not user.otp_enabled:
......
...@@ -54,6 +54,7 @@ pyasn1==0.4.2 ...@@ -54,6 +54,7 @@ pyasn1==0.4.2
pycparser==2.18 pycparser==2.18
pycrypto==2.6.1 pycrypto==2.6.1
pyldap==2.4.45 pyldap==2.4.45
pyotp==2.2.6
PyNaCl==1.2.1 PyNaCl==1.2.1
python-dateutil==2.6.1 python-dateutil==2.6.1
python-gssapi==0.6.4 python-gssapi==0.6.4
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment