diff --git a/authentik/stages/authenticator_totp/forms.py b/authentik/stages/authenticator_totp/forms.py
index 91d635c92..98ebe481e 100644
--- a/authentik/stages/authenticator_totp/forms.py
+++ b/authentik/stages/authenticator_totp/forms.py
@@ -1,54 +1,9 @@
"""OTP Time forms"""
from django import forms
-from django.utils.safestring import mark_safe
-from django.utils.translation import gettext_lazy as _
-from django_otp.models import Device
from authentik.stages.authenticator_totp.models import AuthenticatorTOTPStage
-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"
{value}") # nosec
-
-
-class SetupForm(forms.Form):
- """Form to setup Time-based OTP"""
-
- device: Device = None
-
- qr_code = forms.CharField(
- widget=PictureWidget,
- disabled=True,
- required=False,
- label=_("Scan this Code with your OTP App."),
- )
- code = forms.CharField(
- label=_("Please enter the Token on your device."),
- widget=forms.TextInput(
- attrs={
- "autocomplete": "off",
- "placeholder": "Code",
- "autofocus": "autofocus",
- }
- ),
- )
-
- def __init__(self, device, qr_code, *args, **kwargs):
- super().__init__(*args, **kwargs)
- self.device = device
- self.fields["qr_code"].initial = qr_code
-
- def clean_code(self):
- """Check code with new otp device"""
- if self.device is not None:
- if not self.device.verify_token(self.cleaned_data.get("code")):
- raise forms.ValidationError(_("OTP Code does not match"))
- return self.cleaned_data.get("code")
-
-
class AuthenticatorTOTPStageForm(forms.ModelForm):
"""OTP Time-based Stage setup form"""
diff --git a/authentik/stages/authenticator_totp/stage.py b/authentik/stages/authenticator_totp/stage.py
index cfada422b..7c51e4993 100644
--- a/authentik/stages/authenticator_totp/stage.py
+++ b/authentik/stages/authenticator_totp/stage.py
@@ -1,43 +1,66 @@
"""TOTP Setup stage"""
-from typing import Any
-
from django.http import HttpRequest, HttpResponse
-from django.utils.encoding import force_str
-from django.views.generic import FormView
+from django.http.request import QueryDict
+from django.utils.translation import gettext_lazy as _
from django_otp.plugins.otp_totp.models import TOTPDevice
-from lxml.etree import tostring # nosec
-from qrcode import QRCode
-from qrcode.image.svg import SvgFillImage
+from rest_framework.fields import CharField, IntegerField
+from rest_framework.serializers import ValidationError
from structlog.stdlib import get_logger
+from authentik.flows.challenge import (
+ Challenge,
+ ChallengeResponse,
+ ChallengeTypes,
+ WithUserInfoChallenge,
+)
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER
-from authentik.flows.stage import StageView
-from authentik.stages.authenticator_totp.forms import SetupForm
+from authentik.flows.stage import ChallengeStageView
from authentik.stages.authenticator_totp.models import AuthenticatorTOTPStage
LOGGER = get_logger()
SESSION_TOTP_DEVICE = "totp_device"
-class AuthenticatorTOTPStageView(FormView, StageView):
+class AuthenticatorTOTPChallenge(WithUserInfoChallenge):
+ """TOTP Setup challenge"""
+
+ config_url = CharField()
+
+
+class AuthenticatorTOTPChallengeResponse(ChallengeResponse):
+ """TOTP Challenge response, device is set by get_response_instance"""
+
+ device: TOTPDevice
+
+ code = IntegerField()
+
+ def validate_code(self, code: int) -> int:
+ """Validate totp code"""
+ if self.device is not None:
+ if not self.device.verify_token(code):
+ raise ValidationError(_("OTP Code does not match"))
+ return code
+
+
+class AuthenticatorTOTPStageView(ChallengeStageView):
"""OTP totp Setup stage"""
- form_class = SetupForm
+ response_class = AuthenticatorTOTPChallengeResponse
- def get_form_kwargs(self, **kwargs) -> dict[str, Any]:
- kwargs = super().get_form_kwargs(**kwargs)
+ def get_challenge(self, *args, **kwargs) -> Challenge:
device: TOTPDevice = self.request.session[SESSION_TOTP_DEVICE]
- kwargs["device"] = device
- kwargs["qr_code"] = self._get_qr_code(device)
- return kwargs
+ return AuthenticatorTOTPChallenge(
+ data={
+ "type": ChallengeTypes.native,
+ "component": "ak-stage-authenticator-totp",
+ "config_url": device.config_url,
+ }
+ )
- def _get_qr_code(self, device: TOTPDevice) -> str:
- """Get QR Code SVG as string based on `device`"""
- qr_code = QRCode(image_factory=SvgFillImage)
- qr_code.add_data(device.config_url)
- svg_image = tostring(qr_code.make_image().get_image())
- sr_wrapper = f'