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
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

View file

@ -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(

View file

@ -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'<img src="{value}" />') # nosec
return mark_safe(f"<br>{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:

View file

@ -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
if SESSION_TOTP_DEVICE not in self.request.session:
device = TOTPDevice(user=user, confirmed=True, digits=stage.digits)
self.executor.plan.context[PLAN_CONTEXT_TOTP_DEVICE] = device
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()