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
7 changed files with 86 additions and 25 deletions
Showing only changes of commit 8e419948f1 - Show all commits

View File

@ -409,3 +409,17 @@ class OAuth2ApplicationForm(forms.ModelForm):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
for key in readonly_fields: for key in readonly_fields:
self.fields[key].widget.attrs['readonly'] = True self.fields[key].widget.attrs['readonly'] = True
class DisableMfaForm(forms.Form):
disable_mfa_confirm = forms.BooleanField(
help_text="Confirming disabling of multi-factor authentication",
initial=False,
label=_('Confirm'),
required=True,
widget=forms.CheckboxInput(
attrs={
"autocomplete": "off",
},
),
)

View File

@ -0,0 +1,22 @@
{% extends 'layout.html' %}
{% load pipeline static %}
{% load add_form_classes from forms %}
{% block page_title %}
Disable Multi-factor Authentication
{% endblock %}
{% block body %}
<div class="bid box">
<p>
You are going to disable multi-factor authentication (MFA).
You can always configure MFA again.
</p>
<form method="post">{% csrf_token %}
{% with form=form|add_form_classes %}
{% with field=form.disable_mfa_confirm %}
{% include "components/forms/field.html" %}
{% endwith %}
{% endwith %}
<button type="submit" class="btn-danger">Disable</button>
</form>
{% endblock %}

View File

@ -13,7 +13,7 @@ Multi-factor Authentication Setup
You can disable MFA at any time, but you have to sign-in using your authentication device or a recovery code. You can disable MFA at any time, but you have to sign-in using your authentication device or a recovery code.
</p> </p>
<div> <div>
<button class="btn-danger">Disable</btn> <a class="btn btn-danger" href="{% url 'bid_main:disable_mfa' %}">Disable</a>
</div> </div>
{% else %} {% else %}
<p> <p>

View File

@ -3,7 +3,7 @@ from django.urls import reverse_lazy, path, re_path
from django.contrib.auth import views as auth_views from django.contrib.auth import views as auth_views
from . import forms from . import forms
from .views import normal_pages, registration_email, json_api, developer_applications from .views import mfa, normal_pages, registration_email, json_api, developer_applications
app_name = "bid_main" app_name = "bid_main"
urlpatterns = [ urlpatterns = [
@ -148,9 +148,14 @@ urlpatterns = [
), ),
path( path(
'mfa/', 'mfa/',
normal_pages.MfaView.as_view(), mfa.MfaView.as_view(),
name='mfa', name='mfa',
), ),
path(
'mfa/disable/',
mfa.DisableMfaView.as_view(),
name='disable_mfa',
),
] ]
# Only enable this on a dev server: # Only enable this on a dev server:

42
bid_main/views/mfa.py Normal file
View File

@ -0,0 +1,42 @@
from collections import defaultdict
from django_otp import devices_for_user
from django_otp.plugins.otp_static.models import StaticDevice
from django_otp.plugins.otp_totp.models import TOTPDevice
from django.db import transaction
from django.urls import reverse_lazy
from django.views.generic import TemplateView
from django.views.generic.edit import FormView
from . import mixins
from bid_main.forms import DisableMfaForm
class MfaView(mixins.MfaRequiredIfConfiguredMixin, TemplateView):
template_name = "bid_main/mfa/setup.html"
def get_context_data(self, **kwargs):
devices = list(devices_for_user(self.request.user))
devices_per_category = defaultdict(list)
for device in devices:
if isinstance(device, StaticDevice):
devices_per_category['recovery'].append(device)
if isinstance(device, TOTPDevice):
devices_per_category['totp'].append(device)
return {
'devices_per_category': devices_per_category,
'user_has_mfa_configured': len(devices) > 0,
}
class DisableMfaView(mixins.MfaRequiredMixin, FormView):
form_class = DisableMfaForm
template_name = "bid_main/mfa/disable.html"
success_url = reverse_lazy('bid_main:mfa')
def form_valid(self, form):
with transaction.atomic():
for device in devices_for_user(self.request.user, confirmed=None):
device.delete()
return super().form_valid(form)

View File

@ -4,7 +4,6 @@ No error handlers, no usually-one-off things like registration and
email confirmation. email confirmation.
""" """
from collections import defaultdict
import logging import logging
import urllib.parse import urllib.parse
@ -23,9 +22,6 @@ from django.views.decorators.cache import never_cache
from django.views.generic import TemplateView, FormView from django.views.generic import TemplateView, FormView
from django.views.generic.base import View from django.views.generic.base import View
from django.views.generic.edit import UpdateView from django.views.generic.edit import UpdateView
from django_otp import devices_for_user
from django_otp.plugins.otp_static.models import StaticDevice
from django_otp.plugins.otp_totp.models import TOTPDevice
from otp_agents.forms import OTPTokenForm from otp_agents.forms import OTPTokenForm
import loginas.utils import loginas.utils
import oauth2_provider.models as oauth2_models import oauth2_provider.models as oauth2_models
@ -452,20 +448,3 @@ class TerminateSessionView(mixins.MfaRequiredIfConfiguredMixin, View):
user_session.terminate() user_session.terminate()
return redirect('bid_main:active_sessions') return redirect('bid_main:active_sessions')
return HttpResponseNotFound("session not found") return HttpResponseNotFound("session not found")
class MfaView(mixins.MfaRequiredIfConfiguredMixin, TemplateView):
template_name = "bid_main/mfa_setup.html"
def get_context_data(self, **kwargs):
devices = list(devices_for_user(self.request.user))
devices_per_category = defaultdict(list)
for device in devices:
if isinstance(device, StaticDevice):
devices_per_category['recovery'].append(device)
if isinstance(device, TOTPDevice):
devices_per_category['totp'].append(device)
return {
'devices_per_category': devices_per_category,
'user_has_mfa_configured': len(devices) > 0,
}

View File

@ -58,7 +58,6 @@ INSTALLED_APPS = [
"django.contrib.flatpages", "django.contrib.flatpages",
"django_otp", "django_otp",
"django_otp.plugins.otp_totp", "django_otp.plugins.otp_totp",
"django_otp.plugins.otp_hotp",
"django_otp.plugins.otp_static", "django_otp.plugins.otp_static",
"django_agent_trust", "django_agent_trust",
"oauth2_provider", "oauth2_provider",