stages/otp_time: implement TOTP Setup stage
This commit is contained in:
parent
285a69d91f
commit
b270fb0742
|
@ -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
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
if SESSION_TOTP_DEVICE not in self.request.session:
|
||||||
device = TOTPDevice(user=user, confirmed=True, digits=stage.digits)
|
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)
|
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()
|
||||||
|
|
Reference in a new issue