Initial mfa support (for internal users) #93591
@ -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",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
22
bid_main/templates/bid_main/mfa/disable.html
Normal file
22
bid_main/templates/bid_main/mfa/disable.html
Normal 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 %}
|
@ -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>
|
@ -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
42
bid_main/views/mfa.py
Normal 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)
|
@ -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,
|
|
||||||
}
|
|
||||||
|
@ -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",
|
||||||
|
Loading…
Reference in New Issue
Block a user