perf: passkey auth auto mfa

This commit is contained in:
老广
2025-05-07 16:24:39 +08:00
committed by GitHub
parent 8065e04f26
commit e2830ecdd6
13 changed files with 192 additions and 105 deletions

View File

@@ -1,9 +1,12 @@
import time
from django.conf import settings
from django.http import JsonResponse
from django.shortcuts import render
from django.utils.translation import gettext as _
from rest_framework.decorators import action
from rest_framework.permissions import IsAuthenticated, AllowAny
from rest_framework.response import Response
from authentication.mixins import AuthMixin
from common.api import JMSModelViewSet
@@ -44,6 +47,9 @@ class PasskeyViewSet(AuthMixin, FlashMessageMixin, JMSModelViewSet):
@action(methods=['get'], detail=False, url_path='login', permission_classes=[AllowAny])
def login(self, request):
confirm_mfa = request.GET.get('mfa')
if confirm_mfa:
request.session['passkey_confirm_mfa'] = '1'
return render(request, 'authentication/passkey.html', {})
def redirect_to_error(self, error):
@@ -64,8 +70,16 @@ class PasskeyViewSet(AuthMixin, FlashMessageMixin, JMSModelViewSet):
if not user:
return self.redirect_to_error(_('Auth failed'))
confirm_mfa = request.session.get('passkey_confirm_mfa')
if confirm_mfa:
request.session['CONFIRM_LEVEL'] = ConfirmType.values.index('mfa') + 1
request.session['CONFIRM_TIME'] = int(time.time())
request.session['passkey_confirm_mfa'] = ''
return Response('ok')
try:
self.check_oauth2_auth(user, settings.AUTH_BACKEND_PASSKEY)
self.mark_mfa_ok('passkey', user)
return self.redirect_to_guard_view()
except Exception as e:
msg = getattr(e, 'msg', '') or str(e)

View File

@@ -34,6 +34,7 @@ class MFAType(TextChoices):
Email = 'email', _('Email')
Face = 'face', _('Face Recognition')
Radius = 'otp_radius', _('Radius')
Passkey = 'passkey', _('Passkey')
Custom = 'mfa_custom', _('Custom')

View File

@@ -7,6 +7,7 @@ from django.utils.translation import gettext_lazy as _
class BaseMFA(abc.ABC):
placeholder = _('Please input security code')
skip_cache_check = False
has_code = True
def __init__(self, user):
"""

View File

@@ -11,6 +11,7 @@ class MFAFace(BaseMFA, AuthFaceMixin):
display_name = MFAType.Face.name
placeholder = 'Face Recognition'
skip_cache_check = True
has_code = False
def _check_code(self, code):
assert self.is_authenticated()

View File

@@ -49,4 +49,3 @@ class MFAOtp(BaseMFA):
def help_text_of_disable(self):
return ''

View File

@@ -0,0 +1,46 @@
from django.conf import settings
from django.utils.translation import gettext_lazy as _
from authentication.mfa.base import BaseMFA
from ..const import MFAType
class MFAPasskey(BaseMFA):
name = MFAType.Passkey.value
display_name = MFAType.Passkey.name
placeholder = 'Passkey'
has_code = False
def _check_code(self, code):
assert self.is_authenticated()
return False, ''
def is_active(self):
if not self.is_authenticated():
return True
return self.user.passkey_set.count()
@staticmethod
def global_enabled():
return settings.AUTH_PASSKEY
def get_enable_url(self) -> str:
return '/ui/#/profile/passkeys'
def get_disable_url(self) -> str:
return '/ui/#/profile/passkeys'
def disable(self):
pass
def can_disable(self) -> bool:
return False
@staticmethod
def help_text_of_enable():
return _("Using passkey as MFA")
@staticmethod
def help_text_of_disable():
return _("Using passkey as MFA")

View File

@@ -5,12 +5,13 @@
<head>
<meta charset="UTF-8">
<title>Login passkey</title>
<script src="{% static "js/jquery-3.6.1.min.js" %}?_=9"></script>
<script src="{% static 'js/jquery-3.6.1.min.js' %}?_=9"></script>
</head>
<body>
<form action='{% url 'api-auth:passkey-auth' %}' method="post" id="loginForm">
<input type="hidden" name="passkeys" id="passkeys"/>
</form>
<form action="{% url 'api-auth:passkey-auth' %}" method="post" id="loginForm">
{% csrf_token %}
<input type="hidden" name="passkeys" id="passkeys"/>
</form>
</body>
<script>
const loginUrl = "/core/auth/login/";

View File

@@ -5,11 +5,14 @@ from datetime import datetime, timedelta
from urllib.parse import urljoin, urlparse
from django.conf import settings
from django.shortcuts import reverse
from django.templatetags.static import static
from django.utils.translation import gettext_lazy as _
from audits.models import UserLoginLog
from common.utils import get_ip_city, get_request_ip
from common.utils import get_logger, get_object_or_none
from common.utils import static_or_direct
from users.models import User
from .notifications import DifferentCityLoginMessage
@@ -75,3 +78,72 @@ def check_user_property_is_correct(username, **properties):
user = None
break
return user
def get_auth_methods():
return [
{
'name': 'OpenID',
'enabled': settings.AUTH_OPENID,
'url': reverse('authentication:openid:login'),
'logo': static('img/login_oidc_logo.png'),
'auto_redirect': True # 是否支持自动重定向
},
{
'name': 'CAS',
'enabled': settings.AUTH_CAS,
'url': reverse('authentication:cas:cas-login'),
'logo': static('img/login_cas_logo.png'),
'auto_redirect': True
},
{
'name': 'SAML2',
'enabled': settings.AUTH_SAML2,
'url': reverse('authentication:saml2:saml2-login'),
'logo': static('img/login_saml2_logo.png'),
'auto_redirect': True
},
{
'name': settings.AUTH_OAUTH2_PROVIDER,
'enabled': settings.AUTH_OAUTH2,
'url': reverse('authentication:oauth2:login'),
'logo': static_or_direct(settings.AUTH_OAUTH2_LOGO_PATH),
'auto_redirect': True
},
{
'name': _('WeCom'),
'enabled': settings.AUTH_WECOM,
'url': reverse('authentication:wecom-qr-login'),
'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')
},
{
'name': _('FeiShu'),
'enabled': settings.AUTH_FEISHU,
'url': reverse('authentication:feishu-qr-login'),
'logo': static('img/login_feishu_logo.png')
},
{
'name': 'Lark',
'enabled': settings.AUTH_LARK,
'url': reverse('authentication:lark-qr-login'),
'logo': static('img/login_lark_logo.png')
},
{
'name': _('Slack'),
'enabled': settings.AUTH_SLACK,
'url': reverse('authentication:slack-qr-login'),
'logo': static('img/login_slack_logo.png')
},
{
'name': _("Passkey"),
'enabled': settings.AUTH_PASSKEY,
'url': reverse('api-auth:passkey-login'),
'logo': static('img/login_passkey.png')
}
]

View File

@@ -14,7 +14,6 @@ from django.contrib.auth import login as auth_login, logout as auth_logout
from django.db import IntegrityError
from django.http import HttpRequest
from django.shortcuts import reverse, redirect
from django.templatetags.static import static
from django.urls import reverse_lazy
from django.utils.decorators import method_decorator
from django.utils.translation import gettext as _, get_language
@@ -25,13 +24,14 @@ from django.views.generic.base import TemplateView, RedirectView
from django.views.generic.edit import FormView
from common.const import Language
from common.utils import FlashMessageUtil, static_or_direct, safe_next_url
from common.utils import FlashMessageUtil, safe_next_url
from users.utils import (
redirect_user_first_login_or_index
)
from .. import mixins, errors
from ..const import RSA_PRIVATE_KEY, RSA_PUBLIC_KEY
from ..forms import get_user_login_form_cls
from ..utils import get_auth_methods
__all__ = [
'UserLoginView', 'UserLogoutView',
@@ -46,73 +46,17 @@ class UserLoginContextMixin:
def get_support_auth_methods(self):
query_string = self.request.GET.urlencode()
auth_methods = [
{
'name': 'OpenID',
'enabled': settings.AUTH_OPENID,
'url': f"{reverse('authentication:openid:login')}?{query_string}",
'logo': static('img/login_oidc_logo.png'),
'auto_redirect': True # 是否支持自动重定向
},
{
'name': 'CAS',
'enabled': settings.AUTH_CAS,
'url': f"{reverse('authentication:cas:cas-login')}?{query_string}",
'logo': static('img/login_cas_logo.png'),
'auto_redirect': True
},
{
'name': 'SAML2',
'enabled': settings.AUTH_SAML2,
'url': f"{reverse('authentication:saml2:saml2-login')}?{query_string}",
'logo': static('img/login_saml2_logo.png'),
'auto_redirect': True
},
{
'name': settings.AUTH_OAUTH2_PROVIDER,
'enabled': settings.AUTH_OAUTH2,
'url': f"{reverse('authentication:oauth2:login')}?{query_string}",
'logo': static_or_direct(settings.AUTH_OAUTH2_LOGO_PATH),
'auto_redirect': True
},
{
'name': _('WeCom'),
'enabled': settings.AUTH_WECOM,
'url': f"{reverse('authentication:wecom-qr-login')}?{query_string}",
'logo': static('img/login_wecom_logo.png'),
},
{
'name': _('DingTalk'),
'enabled': settings.AUTH_DINGTALK,
'url': f"{reverse('authentication:dingtalk-qr-login')}?{query_string}",
'logo': static('img/login_dingtalk_logo.png')
},
{
'name': _('FeiShu'),
'enabled': settings.AUTH_FEISHU,
'url': f"{reverse('authentication:feishu-qr-login')}?{query_string}",
'logo': static('img/login_feishu_logo.png')
},
{
'name': 'Lark',
'enabled': settings.AUTH_LARK,
'url': f"{reverse('authentication:lark-qr-login')}?{query_string}",
'logo': static('img/login_lark_logo.png')
},
{
'name': _('Slack'),
'enabled': settings.AUTH_SLACK,
'url': f"{reverse('authentication:slack-qr-login')}?{query_string}",
'logo': static('img/login_slack_logo.png')
},
{
'name': _("Passkey"),
'enabled': settings.AUTH_PASSKEY,
'url': f"{reverse('api-auth:passkey-login')}?{query_string}",
'logo': static('img/login_passkey.png')
}
]
return [method for method in auth_methods if method['enabled']]
all_methods = get_auth_methods()
methods = []
for method in all_methods:
method = method.copy()
if not method.get('enabled', False):
continue
url = method.get('url', '')
if query_string and url:
method['url'] = '{}?{}'.format(url, query_string)
methods.append(method)
return methods
@staticmethod
def get_support_langs():

View File

@@ -40,6 +40,8 @@ class UserLoginMFAView(mixins.AuthMixin, FormView):
if mfa_type == MFAType.Face:
return redirect(reverse('authentication:login-face-capture'))
elif mfa_type == MFAType.Passkey:
return redirect('/api/v1/authentication/passkeys/login/')
return self.do_mfa_check(form, code, mfa_type)
def do_mfa_check(self, form, code, mfa_type):