219 lines
7.6 KiB
Python
219 lines
7.6 KiB
Python
from django import forms
|
|
from django.contrib import messages
|
|
from django.contrib.auth.decorators import login_required
|
|
from django.http import Http404, HttpResponseRedirect
|
|
from django.shortcuts import get_object_or_404
|
|
from django.urls import reverse, reverse_lazy
|
|
from django.utils.decorators import method_decorator
|
|
from django.views.generic import TemplateView
|
|
from django.views.generic.edit import FormView
|
|
|
|
from allauth.account import app_settings as account_settings
|
|
from allauth.account.adapter import get_adapter as get_account_adapter
|
|
from allauth.account.decorators import reauthentication_required
|
|
from allauth.account.stages import LoginStageController
|
|
from allauth.mfa import app_settings, totp
|
|
from allauth.mfa.adapter import get_adapter
|
|
from allauth.mfa.forms import ActivateTOTPForm, AuthenticateForm
|
|
from allauth.mfa.models import Authenticator
|
|
from allauth.mfa.recovery_codes import RecoveryCodes
|
|
from allauth.mfa.stages import AuthenticateStage
|
|
from allauth.mfa.utils import is_mfa_enabled
|
|
|
|
|
|
class AuthenticateView(FormView):
|
|
form_class = AuthenticateForm
|
|
template_name = "mfa/authenticate." + account_settings.TEMPLATE_EXTENSION
|
|
|
|
def dispatch(self, request, *args, **kwargs):
|
|
self.stage = LoginStageController.enter(request, AuthenticateStage.key)
|
|
if not self.stage or not is_mfa_enabled(
|
|
self.stage.login.user, [Authenticator.Type.TOTP]
|
|
):
|
|
return HttpResponseRedirect(reverse("account_login"))
|
|
return super().dispatch(request, *args, **kwargs)
|
|
|
|
def get_form_kwargs(self):
|
|
ret = super().get_form_kwargs()
|
|
ret["user"] = self.stage.login.user
|
|
return ret
|
|
|
|
def form_valid(self, form):
|
|
return self.stage.exit()
|
|
|
|
|
|
authenticate = AuthenticateView.as_view()
|
|
|
|
|
|
@method_decorator(login_required, name="dispatch")
|
|
class IndexView(TemplateView):
|
|
template_name = "mfa/index." + account_settings.TEMPLATE_EXTENSION
|
|
|
|
def get_context_data(self, **kwargs):
|
|
ret = super().get_context_data(**kwargs)
|
|
authenticators = {
|
|
auth.type: auth.wrap()
|
|
for auth in Authenticator.objects.filter(user=self.request.user)
|
|
}
|
|
ret["authenticators"] = authenticators
|
|
return ret
|
|
|
|
|
|
index = IndexView.as_view()
|
|
|
|
|
|
@method_decorator(reauthentication_required, name="dispatch")
|
|
class ActivateTOTPView(FormView):
|
|
form_class = ActivateTOTPForm
|
|
template_name = "mfa/totp/activate_form." + account_settings.TEMPLATE_EXTENSION
|
|
success_url = reverse_lazy("mfa_view_recovery_codes")
|
|
|
|
def dispatch(self, request, *args, **kwargs):
|
|
if is_mfa_enabled(request.user, [Authenticator.Type.TOTP]):
|
|
return HttpResponseRedirect(reverse("mfa_deactivate_totp"))
|
|
return super().dispatch(request, *args, **kwargs)
|
|
|
|
def get_context_data(self, **kwargs):
|
|
ret = super().get_context_data(**kwargs)
|
|
adapter = get_adapter()
|
|
totp_url = totp.build_totp_url(
|
|
adapter.get_totp_label(self.request.user),
|
|
adapter.get_totp_issuer(),
|
|
ret["form"].secret,
|
|
)
|
|
totp_svg = totp.build_totp_svg(totp_url)
|
|
ret.update(
|
|
{
|
|
"totp_svg": totp_svg,
|
|
"totp_url": totp_url,
|
|
}
|
|
)
|
|
return ret
|
|
|
|
def get_form_kwargs(self):
|
|
ret = super().get_form_kwargs()
|
|
ret["user"] = self.request.user
|
|
return ret
|
|
|
|
def form_valid(self, form):
|
|
totp.TOTP.activate(self.request.user, form.secret)
|
|
RecoveryCodes.activate(self.request.user)
|
|
adapter = get_account_adapter(self.request)
|
|
adapter.add_message(
|
|
self.request, messages.SUCCESS, "mfa/messages/totp_activated.txt"
|
|
)
|
|
return super().form_valid(form)
|
|
|
|
|
|
activate_totp = ActivateTOTPView.as_view()
|
|
|
|
|
|
@method_decorator(login_required, name="dispatch")
|
|
class DeactivateTOTPView(FormView):
|
|
form_class = forms.Form
|
|
template_name = "mfa/totp/deactivate_form." + account_settings.TEMPLATE_EXTENSION
|
|
success_url = reverse_lazy("mfa_index")
|
|
|
|
def dispatch(self, request, *args, **kwargs):
|
|
self.authenticator = get_object_or_404(
|
|
Authenticator,
|
|
user=self.request.user,
|
|
type=Authenticator.Type.TOTP,
|
|
)
|
|
if not is_mfa_enabled(request.user, [Authenticator.Type.TOTP]):
|
|
return HttpResponseRedirect(reverse("mfa_activate_totp"))
|
|
return self._dispatch(request, *args, **kwargs)
|
|
|
|
@method_decorator(reauthentication_required)
|
|
def _dispatch(self, request, *args, **kwargs):
|
|
"""There's no point to reauthenticate when MFA is not enabled, so the
|
|
`is_mfa_enabled` chheck needs to go first, which is why we cannot slap a
|
|
`reauthentication_required` decorator on the `dispatch` directly.
|
|
"""
|
|
return super().dispatch(request, *args, **kwargs)
|
|
|
|
def form_valid(self, form):
|
|
self.authenticator.wrap().deactivate()
|
|
adapter = get_account_adapter(self.request)
|
|
adapter.add_message(
|
|
self.request, messages.SUCCESS, "mfa/messages/totp_deactivated.txt"
|
|
)
|
|
return super().form_valid(form)
|
|
|
|
|
|
deactivate_totp = DeactivateTOTPView.as_view()
|
|
|
|
|
|
@method_decorator(reauthentication_required, name="dispatch")
|
|
class GenerateRecoveryCodesView(FormView):
|
|
form_class = forms.Form
|
|
template_name = "mfa/recovery_codes/generate." + account_settings.TEMPLATE_EXTENSION
|
|
success_url = reverse_lazy("mfa_view_recovery_codes")
|
|
|
|
def form_valid(self, form):
|
|
Authenticator.objects.filter(
|
|
user=self.request.user, type=Authenticator.Type.RECOVERY_CODES
|
|
).delete()
|
|
RecoveryCodes.activate(self.request.user)
|
|
adapter = get_account_adapter(self.request)
|
|
adapter.add_message(
|
|
self.request, messages.SUCCESS, "mfa/messages/recovery_codes_generated.txt"
|
|
)
|
|
return super().form_valid(form)
|
|
|
|
|
|
generate_recovery_codes = GenerateRecoveryCodesView.as_view()
|
|
|
|
|
|
@method_decorator(reauthentication_required, name="dispatch")
|
|
class DownloadRecoveryCodesView(TemplateView):
|
|
template_name = "mfa/recovery_codes/download.txt"
|
|
content_type = "text/plain"
|
|
|
|
def dispatch(self, request, *args, **kwargs):
|
|
self.authenticator = get_object_or_404(
|
|
Authenticator,
|
|
user=self.request.user,
|
|
type=Authenticator.Type.RECOVERY_CODES,
|
|
)
|
|
self.unused_codes = self.authenticator.wrap().get_unused_codes()
|
|
if not self.unused_codes:
|
|
return Http404()
|
|
return super().dispatch(request, *args, **kwargs)
|
|
|
|
def get_context_data(self, **kwargs):
|
|
ret = super().get_context_data(**kwargs)
|
|
ret["unused_codes"] = self.unused_codes
|
|
return ret
|
|
|
|
def render_to_response(self, context, **response_kwargs):
|
|
response = super().render_to_response(context, **response_kwargs)
|
|
response["Content-Disposition"] = 'attachment; filename="recovery-codes.txt"'
|
|
return response
|
|
|
|
|
|
download_recovery_codes = DownloadRecoveryCodesView.as_view()
|
|
|
|
|
|
@method_decorator(reauthentication_required, name="dispatch")
|
|
class ViewRecoveryCodesView(TemplateView):
|
|
template_name = "mfa/recovery_codes/index." + account_settings.TEMPLATE_EXTENSION
|
|
|
|
def get_context_data(self, **kwargs):
|
|
ret = super().get_context_data(**kwargs)
|
|
authenticator = get_object_or_404(
|
|
Authenticator,
|
|
user=self.request.user,
|
|
type=Authenticator.Type.RECOVERY_CODES,
|
|
)
|
|
ret.update(
|
|
{
|
|
"unused_codes": authenticator.wrap().get_unused_codes(),
|
|
"total_count": app_settings.RECOVERY_CODE_COUNT,
|
|
}
|
|
)
|
|
return ret
|
|
|
|
|
|
view_recovery_codes = ViewRecoveryCodesView.as_view()
|