From b270fb0742571b151360bb1e7255bd9e08fe7373 Mon Sep 17 00:00:00 2001 From: Jens Langhammer Date: Tue, 30 Jun 2020 12:14:40 +0200 Subject: [PATCH] stages/otp_time: implement TOTP Setup stage --- .../templatetags/passbook_user_settings.py | 2 +- passbook/flows/views.py | 2 ++ passbook/stages/otp_time/forms.py | 6 +++- passbook/stages/otp_time/stage.py | 28 +++++++++++-------- 4 files changed, 24 insertions(+), 14 deletions(-) diff --git a/passbook/core/templatetags/passbook_user_settings.py b/passbook/core/templatetags/passbook_user_settings.py index 6cbbd29db..1af1b9c97 100644 --- a/passbook/core/templatetags/passbook_user_settings.py +++ b/passbook/core/templatetags/passbook_user_settings.py @@ -16,7 +16,7 @@ register = template.Library() # pylint: disable=unused-argument def user_stages(context: RequestContext) -> List[UIUserSettings]: """Return list of all stages which apply to user""" - _all_stages: Iterable[Stage] = Stage.__subclasses__() + _all_stages: Iterable[Stage] = Stage.objects.all().select_subclasses() matching_stages: List[UIUserSettings] = [] for stage in _all_stages: user_settings = stage.ui_user_settings diff --git a/passbook/flows/views.py b/passbook/flows/views.py index 0e6d0fef5..1fe969f4a 100644 --- a/passbook/flows/views.py +++ b/passbook/flows/views.py @@ -111,6 +111,7 @@ class FlowExecutorView(View): stage_response = self.current_stage_view.get(request, *args, **kwargs) return to_stage_response(request, stage_response) except Exception as exc: # pylint: disable=broad-except + LOGGER.exception(exc) return to_stage_response( request, render( @@ -132,6 +133,7 @@ class FlowExecutorView(View): stage_response = self.current_stage_view.post(request, *args, **kwargs) return to_stage_response(request, stage_response) except Exception as exc: # pylint: disable=broad-except + LOGGER.exception(exc) return to_stage_response( request, render( diff --git a/passbook/stages/otp_time/forms.py b/passbook/stages/otp_time/forms.py index 5f590d773..af991fa06 100644 --- a/passbook/stages/otp_time/forms.py +++ b/passbook/stages/otp_time/forms.py @@ -12,7 +12,7 @@ class PictureWidget(forms.widgets.Widget): """Widget to render value as img-tag""" def render(self, name, value, attrs=None, renderer=None): - return mark_safe(f'') # nosec + return mark_safe(f"
{value}") # nosec class SetupForm(forms.Form): @@ -33,6 +33,10 @@ class SetupForm(forms.Form): widget=forms.TextInput(attrs={"placeholder": _("One-Time Password")}), ) + def __init__(self, device, qr_code, *args, **kwargs): + super().__init__(*args, **kwargs) + self.fields["qr_code"].initial = qr_code + def clean_code(self): """Check code with new otp device""" if self.device is not None: diff --git a/passbook/stages/otp_time/stage.py b/passbook/stages/otp_time/stage.py index a8bea5fd7..1f756dcff 100644 --- a/passbook/stages/otp_time/stage.py +++ b/passbook/stages/otp_time/stage.py @@ -1,16 +1,17 @@ +"""TOTP Setup stage""" from base64 import b32encode from binascii import unhexlify from typing import Any, Dict -from django.contrib import messages +import lxml.etree as ET # nosec from django.http import HttpRequest, HttpResponse +from django.utils.encoding import force_text from django.utils.http import urlencode from django.utils.translation import gettext as _ from django.views.generic import FormView -from django_otp import match_token, user_has_device from django_otp.plugins.otp_totp.models import TOTPDevice -from qrcode import make -from qrcode.image.svg import SvgPathImage +from qrcode import QRCode +from qrcode.image.svg import SvgFillImage from structlog import get_logger from passbook.flows.models import NotConfiguredAction, Stage @@ -20,7 +21,7 @@ from passbook.stages.otp_time.forms import SetupForm from passbook.stages.otp_time.models import OTPTimeStage LOGGER = get_logger() -PLAN_CONTEXT_TOTP_DEVICE = "totp_device" +SESSION_TOTP_DEVICE = "totp_device" def otp_auth_url(device: TOTPDevice) -> str: @@ -48,7 +49,7 @@ class OTPTimeStageView(FormView, StageView): def get_form_kwargs(self, **kwargs) -> Dict[str, Any]: kwargs = super().get_form_kwargs(**kwargs) - device: TOTPDevice = self.executor.plan.context[PLAN_CONTEXT_TOTP_DEVICE] + device: TOTPDevice = self.request.session[SESSION_TOTP_DEVICE] kwargs["device"] = device kwargs["qr_code"] = self._get_qr_code(device) return kwargs @@ -56,9 +57,9 @@ class OTPTimeStageView(FormView, StageView): def _get_qr_code(self, device: TOTPDevice) -> str: """Get QR Code SVG as string based on `device`""" url = otp_auth_url(device) - # Make and return QR code - img = make(url, image_factory=SvgPathImage) - return img._img + qr_code = QRCode(image_factory=SvgFillImage) + qr_code.add_data(url) + return force_text(ET.tostring(qr_code.make_image().get_image())) def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse: user = self.executor.plan.context.get(PLAN_CONTEXT_PENDING_USER) @@ -67,13 +68,16 @@ class OTPTimeStageView(FormView, StageView): return self.executor.stage_ok() stage: OTPTimeStage = self.executor.current_stage - device = TOTPDevice(user=user, confirmed=True, digits=stage.digits) - self.executor.plan.context[PLAN_CONTEXT_TOTP_DEVICE] = device + if SESSION_TOTP_DEVICE not in self.request.session: + device = TOTPDevice(user=user, confirmed=True, digits=stage.digits) + + self.request.session[SESSION_TOTP_DEVICE] = device return super().get(request, *args, **kwargs) def form_valid(self, form: SetupForm) -> HttpResponse: """Verify OTP Token""" - device: TOTPDevice = self.executor.plan.context[PLAN_CONTEXT_TOTP_DEVICE] + device: TOTPDevice = self.request.session[SESSION_TOTP_DEVICE] device.save() + del self.request.session[SESSION_TOTP_DEVICE] return self.executor.stage_ok()