stages/otp_time: implement TOTP Setup stage

This commit is contained in:
Jens Langhammer 2020-06-30 12:14:40 +02:00
parent 285a69d91f
commit b270fb0742
4 changed files with 24 additions and 14 deletions

View file

@ -16,7 +16,7 @@ register = template.Library()
# pylint: disable=unused-argument # pylint: disable=unused-argument
def user_stages(context: RequestContext) -> List[UIUserSettings]: def user_stages(context: RequestContext) -> List[UIUserSettings]:
"""Return list of all stages which apply to user""" """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] = [] matching_stages: List[UIUserSettings] = []
for stage in _all_stages: for stage in _all_stages:
user_settings = stage.ui_user_settings user_settings = stage.ui_user_settings

View file

@ -111,6 +111,7 @@ class FlowExecutorView(View):
stage_response = self.current_stage_view.get(request, *args, **kwargs) stage_response = self.current_stage_view.get(request, *args, **kwargs)
return to_stage_response(request, stage_response) return to_stage_response(request, stage_response)
except Exception as exc: # pylint: disable=broad-except except Exception as exc: # pylint: disable=broad-except
LOGGER.exception(exc)
return to_stage_response( return to_stage_response(
request, request,
render( render(
@ -132,6 +133,7 @@ class FlowExecutorView(View):
stage_response = self.current_stage_view.post(request, *args, **kwargs) stage_response = self.current_stage_view.post(request, *args, **kwargs)
return to_stage_response(request, stage_response) return to_stage_response(request, stage_response)
except Exception as exc: # pylint: disable=broad-except except Exception as exc: # pylint: disable=broad-except
LOGGER.exception(exc)
return to_stage_response( return to_stage_response(
request, request,
render( render(

View file

@ -12,7 +12,7 @@ class PictureWidget(forms.widgets.Widget):
"""Widget to render value as img-tag""" """Widget to render value as img-tag"""
def render(self, name, value, attrs=None, renderer=None): def render(self, name, value, attrs=None, renderer=None):
return mark_safe(f'<img src="{value}" />') # nosec return mark_safe(f"<br>{value}") # nosec
class SetupForm(forms.Form): class SetupForm(forms.Form):
@ -33,6 +33,10 @@ class SetupForm(forms.Form):
widget=forms.TextInput(attrs={"placeholder": _("One-Time Password")}), 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): def clean_code(self):
"""Check code with new otp device""" """Check code with new otp device"""
if self.device is not None: if self.device is not None:

View file

@ -1,16 +1,17 @@
"""TOTP Setup stage"""
from base64 import b32encode from base64 import b32encode
from binascii import unhexlify from binascii import unhexlify
from typing import Any, Dict from typing import Any, Dict
from django.contrib import messages import lxml.etree as ET # nosec
from django.http import HttpRequest, HttpResponse from django.http import HttpRequest, HttpResponse
from django.utils.encoding import force_text
from django.utils.http import urlencode from django.utils.http import urlencode
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from django.views.generic import FormView 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 django_otp.plugins.otp_totp.models import TOTPDevice
from qrcode import make from qrcode import QRCode
from qrcode.image.svg import SvgPathImage from qrcode.image.svg import SvgFillImage
from structlog import get_logger from structlog import get_logger
from passbook.flows.models import NotConfiguredAction, Stage 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 from passbook.stages.otp_time.models import OTPTimeStage
LOGGER = get_logger() LOGGER = get_logger()
PLAN_CONTEXT_TOTP_DEVICE = "totp_device" SESSION_TOTP_DEVICE = "totp_device"
def otp_auth_url(device: TOTPDevice) -> str: def otp_auth_url(device: TOTPDevice) -> str:
@ -48,7 +49,7 @@ class OTPTimeStageView(FormView, StageView):
def get_form_kwargs(self, **kwargs) -> Dict[str, Any]: def get_form_kwargs(self, **kwargs) -> Dict[str, Any]:
kwargs = super().get_form_kwargs(**kwargs) 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["device"] = device
kwargs["qr_code"] = self._get_qr_code(device) kwargs["qr_code"] = self._get_qr_code(device)
return kwargs return kwargs
@ -56,9 +57,9 @@ class OTPTimeStageView(FormView, StageView):
def _get_qr_code(self, device: TOTPDevice) -> str: def _get_qr_code(self, device: TOTPDevice) -> str:
"""Get QR Code SVG as string based on `device`""" """Get QR Code SVG as string based on `device`"""
url = otp_auth_url(device) url = otp_auth_url(device)
# Make and return QR code qr_code = QRCode(image_factory=SvgFillImage)
img = make(url, image_factory=SvgPathImage) qr_code.add_data(url)
return img._img return force_text(ET.tostring(qr_code.make_image().get_image()))
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse: def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
user = self.executor.plan.context.get(PLAN_CONTEXT_PENDING_USER) user = self.executor.plan.context.get(PLAN_CONTEXT_PENDING_USER)
@ -67,13 +68,16 @@ class OTPTimeStageView(FormView, StageView):
return self.executor.stage_ok() return self.executor.stage_ok()
stage: OTPTimeStage = self.executor.current_stage 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) return super().get(request, *args, **kwargs)
def form_valid(self, form: SetupForm) -> HttpResponse: def form_valid(self, form: SetupForm) -> HttpResponse:
"""Verify OTP Token""" """Verify OTP Token"""
device: TOTPDevice = self.executor.plan.context[PLAN_CONTEXT_TOTP_DEVICE] device: TOTPDevice = self.request.session[SESSION_TOTP_DEVICE]
device.save() device.save()
del self.request.session[SESSION_TOTP_DEVICE]
return self.executor.stage_ok() return self.executor.stage_ok()