Initial mfa support (for internal users) #93591

Merged
Oleg-Komarov merged 46 commits from mfa into main 2024-08-29 11:44:06 +02:00
3 changed files with 25 additions and 14 deletions
Showing only changes of commit ac496f040a - Show all commits

View File

@ -8,7 +8,7 @@
{% with form=form|add_form_classes %} {% with form=form|add_form_classes %}
<form method="POST" id="u2f-authenticate-form">{% csrf_token %} <form method="POST" id="u2f-authenticate-form">{% csrf_token %}
<fieldset class="mb-4"> <fieldset class="mb-4">
<p>Use a security key.</p> <p>Please use a security key you have configured. Tick the checkbox below before using the key</p>
{% with field=form.otp_trust_agent %} {% with field=form.otp_trust_agent %}
{% include "components/forms/field.html" with with_help_text=True %} {% include "components/forms/field.html" with with_help_text=True %}
{% endwith %} {% endwith %}
@ -23,6 +23,7 @@
{% endwith %} {% endwith %}
</fieldset> </fieldset>
{{ form.non_field_errors }} {{ form.non_field_errors }}
<div id="webauthn-error"></div>
</form> </form>
{% endwith %} {% endwith %}
{% if devices.totp %} {% if devices.totp %}
@ -35,9 +36,13 @@
const form = document.getElementById('u2f-authenticate-form'); const form = document.getElementById('u2f-authenticate-form');
const responseInput = document.getElementById('id_response'); const responseInput = document.getElementById('id_response');
const requestOptions = JSON.parse(responseInput.getAttribute('request-options')); const requestOptions = JSON.parse(responseInput.getAttribute('request-options'));
(async function() { webauthnJSON.get(requestOptions).then(
const credential = await webauthnJSON.get(requestOptions); (credential) => {
responseInput.value = JSON.stringify(credential); responseInput.value = JSON.stringify(credential);
form.submit(); form.submit();
})(); },
(error) => {
document.getElementById('webauthn-error').innerText = 'Something went wrong: ' + error;
}
);
</script> </script>

View File

@ -3,11 +3,13 @@
{% block page_title %}Sign in{% endblock %} {% block page_title %}Sign in{% endblock %}
{% block form %} {% block form %}
{% if is_authentication_form %} {% if form_type == 'login' %}
{% include 'bid_main/components/login_form.html' %} {% include 'bid_main/components/login_form.html' %}
{% elif is_mfa_form %} {% elif form_type == 'mfa' %}
{% include 'bid_main/components/mfa_form.html' %} {% include 'bid_main/components/mfa_form.html' %}
{% elif is_u2f_form %} {% elif form_type == 'u2f' %}
{% include 'bid_main/components/u2f_form.html' %} {% include 'bid_main/components/u2f_form.html' %}
{% else %}
<div class="bix box">Something went wrong</div>
{% endif %} {% endif %}
{% endblock form %} {% endblock form %}

View File

@ -10,7 +10,7 @@ import urllib.parse
from django.conf import settings from django.conf import settings
from django.contrib.auth import views as auth_views, logout, get_user_model from django.contrib.auth import views as auth_views, logout, get_user_model
from django.core.exceptions import ValidationError from django.core.exceptions import ImproperlyConfigured, ValidationError
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from django.db import transaction, IntegrityError from django.db import transaction, IntegrityError
from django.db.models import Count from django.db.models import Count
@ -96,15 +96,19 @@ class LoginView(mixins.RedirectToPrivacyAgreeMixin, mixins.PageIdMixin, otp_agen
ctx = super().get_context_data(**kwargs) ctx = super().get_context_data(**kwargs)
self.find_oauth_flow(ctx) self.find_oauth_flow(ctx)
form = self.get_form() form = self.get_form()
form_type = None
if isinstance(form, U2fAuthenticateForm): if isinstance(form, U2fAuthenticateForm):
ctx["devices"] = self.request.user.mfa_devices_per_category() ctx["devices"] = self.request.user.mfa_devices_per_category()
ctx["is_u2f_form"] = True form_type = 'u2f'
elif isinstance(form, MfaAuthenticateForm): elif isinstance(form, MfaAuthenticateForm):
ctx["devices"] = self.request.user.mfa_devices_per_category() ctx["devices"] = self.request.user.mfa_devices_per_category()
ctx["is_mfa_form"] = True
ctx["use_recovery"] = self._use_recovery() ctx["use_recovery"] = self._use_recovery()
form_type = 'mfa'
elif isinstance(form, forms.AuthenticationForm):
form_type = 'login'
else: else:
ctx["is_authentication_form"] = isinstance(form, forms.AuthenticationForm) raise ImproperlyConfigured('unexpected form object')
ctx["form_type"] = form_type
return ctx return ctx
def get_form(self): def get_form(self):
@ -120,7 +124,7 @@ class LoginView(mixins.RedirectToPrivacyAgreeMixin, mixins.PageIdMixin, otp_agen
kwargs['rp_id'] = rp_id kwargs['rp_id'] = rp_id
kwargs['state'] = dict(state) kwargs['state'] = dict(state)
return U2fAuthenticateForm(**kwargs) return U2fAuthenticateForm(**kwargs)
# this will switch between MfaForm and AuthenticationForm # this will switch between MfaAuthenticateForm and AuthenticationForm
return super().get_form() return super().get_form()
def get_form_kwargs(self): def get_form_kwargs(self):