From eb01b42425768bb4ec85304650327d8fd75ff330 Mon Sep 17 00:00:00 2001 From: Jens Langhammer Date: Wed, 17 Feb 2021 23:52:49 +0100 Subject: [PATCH] flows: mount executor under api, implement initial challenge design --- authentik/api/v2/urls.py | 6 + authentik/core/templates/base/skeleton.html | 1 - authentik/core/templates/shell.html | 4 + authentik/flows/challenge.py | 37 ++++++ authentik/flows/stage.py | 31 +++++ authentik/flows/templates/flows/shell.html | 7 +- authentik/flows/tests/test_planner.py | 12 +- authentik/flows/tests/test_views.py | 22 ++-- authentik/flows/views.py | 38 ++++-- .../stages/authenticator_validate/stage.py | 52 ++++++-- authentik/stages/captcha/tests.py | 2 +- authentik/stages/consent/tests.py | 6 +- authentik/stages/dummy/tests.py | 6 +- authentik/stages/email/tests/test_sending.py | 4 +- authentik/stages/email/tests/test_stage.py | 8 +- authentik/stages/identification/stage.py | 109 +++++++++-------- authentik/stages/identification/tests.py | 14 +-- authentik/stages/invitation/tests.py | 10 +- .../templates/stages/password/flow-form.html | 2 +- authentik/stages/password/tests.py | 14 +-- authentik/stages/prompt/tests.py | 6 +- authentik/stages/user_delete/tests.py | 10 +- authentik/stages/user_login/tests.py | 12 +- authentik/stages/user_logout/tests.py | 4 +- authentik/stages/user_write/tests.py | 12 +- web/rollup.config.js | 29 +++++ .../AuthenticatorValidateStage.ts | 10 ++ web/src/elements/stages/base.ts | 10 ++ .../identification/IdentificationStage.ts | 101 ++++++++++++++++ web/src/flow.ts | 3 + web/src/main.ts | 3 +- web/src/pages/flows/BoundStagesList.ts | 1 + .../{FlowShellCard.ts => FlowExecutor.ts} | 114 +++++++++--------- 33 files changed, 482 insertions(+), 218 deletions(-) create mode 100644 authentik/flows/challenge.py create mode 100644 web/src/elements/stages/authenticator_validate/AuthenticatorValidateStage.ts create mode 100644 web/src/elements/stages/base.ts create mode 100644 web/src/elements/stages/identification/IdentificationStage.ts create mode 100644 web/src/flow.ts rename web/src/pages/generic/{FlowShellCard.ts => FlowExecutor.ts} (68%) diff --git a/authentik/api/v2/urls.py b/authentik/api/v2/urls.py index aaa1355c6c..8416166ffb 100644 --- a/authentik/api/v2/urls.py +++ b/authentik/api/v2/urls.py @@ -29,6 +29,7 @@ from authentik.flows.api import ( FlowViewSet, StageViewSet, ) +from authentik.flows.views import FlowExecutorView from authentik.outposts.api.outpost_service_connections import ( DockerServiceConnectionViewSet, KubernetesServiceConnectionViewSet, @@ -184,4 +185,9 @@ urlpatterns = [ name="schema-swagger-ui", ), path("redoc/", SchemaView.with_ui("redoc", cache_timeout=0), name="schema-redoc"), + path( + "flows/executor//", + FlowExecutorView.as_view(), + name="flow-executor", + ), ] + router.urls diff --git a/authentik/core/templates/base/skeleton.html b/authentik/core/templates/base/skeleton.html index da5ef7786e..5944f41604 100644 --- a/authentik/core/templates/base/skeleton.html +++ b/authentik/core/templates/base/skeleton.html @@ -16,7 +16,6 @@ - {% block head %} {% endblock %} diff --git a/authentik/core/templates/shell.html b/authentik/core/templates/shell.html index 4d4ff3b668..d57edfdc1d 100644 --- a/authentik/core/templates/shell.html +++ b/authentik/core/templates/shell.html @@ -1,5 +1,9 @@ {% extends "base/skeleton.html" %} +{% block head %} + +{% endblock %} + {% block body %} {% endblock %} diff --git a/authentik/flows/challenge.py b/authentik/flows/challenge.py new file mode 100644 index 0000000000..e503c9dc2d --- /dev/null +++ b/authentik/flows/challenge.py @@ -0,0 +1,37 @@ +from enum import Enum +from json.encoder import JSONEncoder + +from django.http import JsonResponse +from rest_framework.fields import ChoiceField, DictField, JSONField +from rest_framework.serializers import CharField, Serializer + + +class ChallengeTypes(Enum): + + native = "native" + shell = "shell" + redirect = "redirect" + + +class Challenge(Serializer): + + type = ChoiceField(choices=list(ChallengeTypes)) + component = CharField(required=False) + args = JSONField() + + +class ChallengeResponse(Serializer): + + pass + + +class ChallengeEncoder(JSONEncoder): + def default(self, obj): + if isinstance(obj, Enum): + return obj.value + return super().default(obj) + + +class HttpChallengeResponse(JsonResponse): + def __init__(self, challenge: Challenge, **kwargs) -> None: + super().__init__(challenge.data, encoder=ChallengeEncoder, **kwargs) diff --git a/authentik/flows/stage.py b/authentik/flows/stage.py index c4e41e1b17..7df187848a 100644 --- a/authentik/flows/stage.py +++ b/authentik/flows/stage.py @@ -3,9 +3,15 @@ from collections import namedtuple from typing import Any, Dict from django.http import HttpRequest +from django.http.response import HttpResponse, JsonResponse from django.utils.translation import gettext_lazy as _ from django.views.generic import TemplateView +from authentik.flows.challenge import ( + Challenge, + ChallengeResponse, + HttpChallengeResponse, +) from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER from authentik.flows.views import FlowExecutorView @@ -43,3 +49,28 @@ class StageView(TemplateView): ) kwargs["primary_action"] = _("Continue") return super().get_context_data(**kwargs) + + +class ChallengeStageView(StageView): + + response_class = ChallengeResponse + + def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse: + challenge = self.get_challenge() + challenge.is_valid() + return HttpChallengeResponse(challenge) + + def post(self, request: HttpRequest, *args, **kwargs) -> HttpResponse: + challenge = self.response_class(data=request.POST) + if not challenge.is_valid(): + return self.challenge_invalid(challenge) + return self.challenge_valid(challenge) + + def get_challenge(self) -> Challenge: + raise NotImplementedError + + def challenge_valid(self, challenge: ChallengeResponse) -> HttpResponse: + raise NotImplementedError + + def challenge_invalid(self, challenge: ChallengeResponse) -> HttpResponse: + return JsonResponse(challenge.errors) diff --git a/authentik/flows/templates/flows/shell.html b/authentik/flows/templates/flows/shell.html index 2d23dbe176..2c46d08338 100644 --- a/authentik/flows/templates/flows/shell.html +++ b/authentik/flows/templates/flows/shell.html @@ -22,11 +22,10 @@ background-position: center; } + {% endblock %} {% block main_container %} - + {% endblock %} diff --git a/authentik/flows/tests/test_planner.py b/authentik/flows/tests/test_planner.py index 415f773f7c..5206ddc5dc 100644 --- a/authentik/flows/tests/test_planner.py +++ b/authentik/flows/tests/test_planner.py @@ -43,7 +43,7 @@ class TestFlowPlanner(TestCase): designation=FlowDesignation.AUTHENTICATION, ) request = self.request_factory.get( - reverse("authentik_flows:flow-executor", kwargs={"flow_slug": flow.slug}), + reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}), ) request.user = get_anonymous_user() @@ -63,7 +63,7 @@ class TestFlowPlanner(TestCase): designation=FlowDesignation.AUTHENTICATION, ) request = self.request_factory.get( - reverse("authentik_flows:flow-executor", kwargs={"flow_slug": flow.slug}), + reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}), ) request.user = get_anonymous_user() @@ -83,7 +83,7 @@ class TestFlowPlanner(TestCase): target=flow, stage=DummyStage.objects.create(name="dummy"), order=0 ) request = self.request_factory.get( - reverse("authentik_flows:flow-executor", kwargs={"flow_slug": flow.slug}), + reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}), ) request.user = get_anonymous_user() @@ -112,7 +112,7 @@ class TestFlowPlanner(TestCase): user = User.objects.create(username="test-user") request = self.request_factory.get( - reverse("authentik_flows:flow-executor", kwargs={"flow_slug": flow.slug}), + reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}), ) request.user = user planner = FlowPlanner(flow) @@ -136,7 +136,7 @@ class TestFlowPlanner(TestCase): ) request = self.request_factory.get( - reverse("authentik_flows:flow-executor", kwargs={"flow_slug": flow.slug}), + reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}), ) request.user = get_anonymous_user() @@ -167,7 +167,7 @@ class TestFlowPlanner(TestCase): PolicyBinding.objects.create(policy=false_policy, target=binding2, order=0) request = self.request_factory.get( - reverse("authentik_flows:flow-executor", kwargs={"flow_slug": flow.slug}), + reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}), ) request.user = get_anonymous_user() diff --git a/authentik/flows/tests/test_views.py b/authentik/flows/tests/test_views.py index a89bea69e9..9e9433fe66 100644 --- a/authentik/flows/tests/test_views.py +++ b/authentik/flows/tests/test_views.py @@ -62,9 +62,7 @@ class TestFlowExecutor(TestCase): cancel_mock = MagicMock() with patch("authentik.flows.views.FlowExecutorView.cancel", cancel_mock): response = self.client.get( - reverse( - "authentik_flows:flow-executor", kwargs={"flow_slug": flow.slug} - ), + reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}), ) self.assertEqual(response.status_code, 302) self.assertEqual(cancel_mock.call_count, 2) @@ -87,7 +85,7 @@ class TestFlowExecutor(TestCase): CONFIG.update_from_dict({"domain": "testserver"}) response = self.client.get( - reverse("authentik_flows:flow-executor", kwargs={"flow_slug": flow.slug}), + reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}), ) self.assertEqual(response.status_code, 200) self.assertIsInstance(response, AccessDeniedResponse) @@ -107,7 +105,7 @@ class TestFlowExecutor(TestCase): CONFIG.update_from_dict({"domain": "testserver"}) response = self.client.get( - reverse("authentik_flows:flow-executor", kwargs={"flow_slug": flow.slug}), + reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}), ) self.assertEqual(response.status_code, 302) self.assertEqual(response.url, reverse("authentik_core:shell")) @@ -126,7 +124,7 @@ class TestFlowExecutor(TestCase): CONFIG.update_from_dict({"domain": "testserver"}) dest = "/unique-string" - url = reverse("authentik_flows:flow-executor", kwargs={"flow_slug": flow.slug}) + url = reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}) response = self.client.get(url + f"?{NEXT_ARG_NAME}={dest}") self.assertEqual(response.status_code, 302) self.assertEqual(response.url, reverse("authentik_core:shell")) @@ -146,7 +144,7 @@ class TestFlowExecutor(TestCase): ) exec_url = reverse( - "authentik_flows:flow-executor", kwargs={"flow_slug": flow.slug} + "authentik_api:flow-executor", kwargs={"flow_slug": flow.slug} ) # First Request, start planning, renders form response = self.client.get(exec_url) @@ -196,7 +194,7 @@ class TestFlowExecutor(TestCase): ): exec_url = reverse( - "authentik_flows:flow-executor", kwargs={"flow_slug": flow.slug} + "authentik_api:flow-executor", kwargs={"flow_slug": flow.slug} ) # First request, run the planner response = self.client.get(exec_url) @@ -250,7 +248,7 @@ class TestFlowExecutor(TestCase): ): exec_url = reverse( - "authentik_flows:flow-executor", kwargs={"flow_slug": flow.slug} + "authentik_api:flow-executor", kwargs={"flow_slug": flow.slug} ) # First request, run the planner response = self.client.get(exec_url) @@ -317,7 +315,7 @@ class TestFlowExecutor(TestCase): ): exec_url = reverse( - "authentik_flows:flow-executor", kwargs={"flow_slug": flow.slug} + "authentik_api:flow-executor", kwargs={"flow_slug": flow.slug} ) # First request, run the planner response = self.client.get(exec_url) @@ -401,7 +399,7 @@ class TestFlowExecutor(TestCase): ): exec_url = reverse( - "authentik_flows:flow-executor", kwargs={"flow_slug": flow.slug} + "authentik_api:flow-executor", kwargs={"flow_slug": flow.slug} ) # First request, run the planner response = self.client.get(exec_url) @@ -455,7 +453,7 @@ class TestFlowExecutor(TestCase): user = User.objects.create(username="test-user") request = self.request_factory.get( - reverse("authentik_flows:flow-executor", kwargs={"flow_slug": flow.slug}), + reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}), ) request.user = user planner = FlowPlanner(flow) diff --git a/authentik/flows/views.py b/authentik/flows/views.py index ab98ba8508..3de5b875c1 100644 --- a/authentik/flows/views.py +++ b/authentik/flows/views.py @@ -19,6 +19,7 @@ from structlog.stdlib import BoundLogger, get_logger from authentik.core.models import USER_ATTRIBUTE_DEBUG from authentik.events.models import cleanse_dict +from authentik.flows.challenge import Challenge, ChallengeTypes, HttpChallengeResponse from authentik.flows.exceptions import EmptyFlowException, FlowNonApplicableException from authentik.flows.models import ConfigurableStage, Flow, FlowDesignation, Stage from authentik.flows.planner import ( @@ -176,7 +177,7 @@ class FlowExecutorView(View): reamining=len(self.plan.stages), ) return redirect_with_qs( - "authentik_flows:flow-executor", self.request.GET, **self.kwargs + "authentik_api:flow-executor", self.request.GET, **self.kwargs ) # User passed all stages self._logger.debug( @@ -246,9 +247,7 @@ class FlowExecutorShellView(TemplateView): def get_context_data(self, **kwargs) -> Dict[str, Any]: flow: Flow = get_object_or_404(Flow, slug=self.kwargs.get("flow_slug")) kwargs["background_url"] = flow.background.url - kwargs["exec_url"] = reverse( - "authentik_flows:flow-executor", kwargs=self.kwargs - ) + kwargs["exec_url"] = reverse("authentik_api:flow-executor", kwargs=self.kwargs) self.request.session[SESSION_KEY_GET] = self.request.GET return kwargs @@ -292,17 +291,38 @@ def to_stage_response(request: HttpRequest, source: HttpResponse) -> HttpRespons if isinstance(source, HttpResponseRedirect) or source.status_code == 302: redirect_url = source["Location"] if request.path != redirect_url: - return JsonResponse({"type": "redirect", "to": redirect_url}) + return HttpChallengeResponse( + Challenge( + {"type": ChallengeTypes.redirect, "args": {"to": redirect_url}} + ) + ) + # return JsonResponse({"type": "redirect", "to": redirect_url}) return source if isinstance(source, TemplateResponse): - return JsonResponse( - {"type": "template", "body": source.render().content.decode("utf-8")} + return HttpChallengeResponse( + Challenge( + { + "type": ChallengeTypes.shell, + "args": {"body": source.render().content.decode("utf-8")}, + } + ) ) + # return JsonResponse( + # {"type": "template", "body": } + # ) # Check for actual HttpResponse (without isinstance as we dont want to check inheritance) if source.__class__ == HttpResponse: - return JsonResponse( - {"type": "template", "body": source.content.decode("utf-8")} + return HttpChallengeResponse( + Challenge( + { + "type": ChallengeTypes.shell, + "args": {"body": source.content.decode("utf-8")}, + } + ) ) + # return JsonResponse( + # {"type": "template", "body": } + # ) return source diff --git a/authentik/stages/authenticator_validate/stage.py b/authentik/stages/authenticator_validate/stage.py index 04c3018b23..07fd4ced85 100644 --- a/authentik/stages/authenticator_validate/stage.py +++ b/authentik/stages/authenticator_validate/stage.py @@ -4,28 +4,37 @@ from typing import Any, Dict from django.http import HttpRequest, HttpResponse from django.views.generic import FormView from django_otp import user_has_device +from rest_framework.fields import IntegerField from structlog.stdlib import get_logger +from authentik.flows.challenge import Challenge, ChallengeResponse, ChallengeTypes from authentik.flows.models import NotConfiguredAction from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER -from authentik.flows.stage import StageView +from authentik.flows.stage import ChallengeStageView, StageView from authentik.stages.authenticator_validate.forms import ValidationForm from authentik.stages.authenticator_validate.models import AuthenticatorValidateStage LOGGER = get_logger() -class AuthenticatorValidateStageView(FormView, StageView): +class CodeChallengeResponse(ChallengeResponse): + + code = IntegerField(min_value=0) + + +class WebAuthnChallengeResponse(ChallengeResponse): + + pass + + +class AuthenticatorValidateStageView(ChallengeStageView): """OTP Validation""" form_class = ValidationForm - def get_form_kwargs(self, **kwargs) -> Dict[str, Any]: - kwargs = super().get_form_kwargs(**kwargs) - kwargs["user"] = self.executor.plan.context.get(PLAN_CONTEXT_PENDING_USER) - return kwargs - def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse: + """Check if a user is set, and check if the user has any devices + if not, we can skip this entire stage""" user = self.executor.plan.context.get(PLAN_CONTEXT_PENDING_USER) if not user: LOGGER.debug("No pending user, continuing") @@ -39,8 +48,27 @@ class AuthenticatorValidateStageView(FormView, StageView): return self.executor.stage_ok() return super().get(request, *args, **kwargs) - def form_valid(self, form: ValidationForm) -> HttpResponse: - """Verify OTP Token""" - # Since we do token checking in the form, we know the token is valid here - # so we can just continue - return self.executor.stage_ok() + # def get_form_kwargs(self, **kwargs) -> Dict[str, Any]: + # kwargs = super().get_form_kwargs(**kwargs) + # kwargs["user"] = self.executor.plan.context.get(PLAN_CONTEXT_PENDING_USER) + # return kwargs + + def get_challenge(self) -> Challenge: + return Challenge( + { + "type": ChallengeTypes.native, + # TODO: use component based on devices + "component": "ak-stage-authenticator-validate", + "args": {"user": "foo.bar.baz"}, + } + ) + + def post_challenge(self, challenge: Challenge) -> HttpResponse: + print(challenge) + return super().post_challenge(challenge) + + # def form_valid(self, form: ValidationForm) -> HttpResponse: + # """Verify OTP Token""" + # # Since we do token checking in the form, we know the token is valid here + # # so we can just continue + # return self.executor.stage_ok() diff --git a/authentik/stages/captcha/tests.py b/authentik/stages/captcha/tests.py index b3664fa371..dcebb1d4e5 100644 --- a/authentik/stages/captcha/tests.py +++ b/authentik/stages/captcha/tests.py @@ -44,7 +44,7 @@ class TestCaptchaStage(TestCase): session.save() response = self.client.post( reverse( - "authentik_flows:flow-executor", kwargs={"flow_slug": self.flow.slug} + "authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug} ), {"g-recaptcha-response": "PASSED"}, ) diff --git a/authentik/stages/consent/tests.py b/authentik/stages/consent/tests.py index 7ab6dbf63c..7a17b42c45 100644 --- a/authentik/stages/consent/tests.py +++ b/authentik/stages/consent/tests.py @@ -45,7 +45,7 @@ class TestConsentStage(TestCase): session[SESSION_KEY_PLAN] = plan session.save() response = self.client.post( - reverse("authentik_flows:flow-executor", kwargs={"flow_slug": flow.slug}), + reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}), {}, ) self.assertEqual(response.status_code, 200) @@ -76,7 +76,7 @@ class TestConsentStage(TestCase): session[SESSION_KEY_PLAN] = plan session.save() response = self.client.post( - reverse("authentik_flows:flow-executor", kwargs={"flow_slug": flow.slug}), + reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}), {}, ) self.assertEqual(response.status_code, 200) @@ -113,7 +113,7 @@ class TestConsentStage(TestCase): session[SESSION_KEY_PLAN] = plan session.save() response = self.client.post( - reverse("authentik_flows:flow-executor", kwargs={"flow_slug": flow.slug}), + reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}), {}, ) self.assertEqual(response.status_code, 200) diff --git a/authentik/stages/dummy/tests.py b/authentik/stages/dummy/tests.py index 61493ead64..97e5a796b7 100644 --- a/authentik/stages/dummy/tests.py +++ b/authentik/stages/dummy/tests.py @@ -34,16 +34,14 @@ class TestDummyStage(TestCase): def test_valid_render(self): """Test that View renders correctly""" response = self.client.get( - reverse( - "authentik_flows:flow-executor", kwargs={"flow_slug": self.flow.slug} - ) + reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}) ) self.assertEqual(response.status_code, 200) def test_post(self): """Test with valid email, check that URL redirects back to itself""" url = reverse( - "authentik_flows:flow-executor", kwargs={"flow_slug": self.flow.slug} + "authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug} ) response = self.client.post(url, {}) self.assertEqual(response.status_code, 200) diff --git a/authentik/stages/email/tests/test_sending.py b/authentik/stages/email/tests/test_sending.py index b97a8ce7c2..fcf4f3f9e2 100644 --- a/authentik/stages/email/tests/test_sending.py +++ b/authentik/stages/email/tests/test_sending.py @@ -46,7 +46,7 @@ class TestEmailStageSending(TestCase): session.save() url = reverse( - "authentik_flows:flow-executor", kwargs={"flow_slug": self.flow.slug} + "authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug} ) with self.settings( EMAIL_BACKEND="django.core.mail.backends.locmem.EmailBackend" @@ -67,7 +67,7 @@ class TestEmailStageSending(TestCase): session.save() url = reverse( - "authentik_flows:flow-executor", kwargs={"flow_slug": self.flow.slug} + "authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug} ) with self.settings( EMAIL_BACKEND="django.core.mail.backends.locmem.EmailBackend" diff --git a/authentik/stages/email/tests/test_stage.py b/authentik/stages/email/tests/test_stage.py index 0a84ecf7ba..bc00dc24b0 100644 --- a/authentik/stages/email/tests/test_stage.py +++ b/authentik/stages/email/tests/test_stage.py @@ -46,7 +46,7 @@ class TestEmailStage(TestCase): session.save() url = reverse( - "authentik_flows:flow-executor", kwargs={"flow_slug": self.flow.slug} + "authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug} ) response = self.client.get(url) self.assertEqual(response.status_code, 200) @@ -61,7 +61,7 @@ class TestEmailStage(TestCase): session.save() url = reverse( - "authentik_flows:flow-executor", kwargs={"flow_slug": self.flow.slug} + "authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug} ) response = self.client.get(url) self.assertEqual(response.status_code, 200) @@ -77,7 +77,7 @@ class TestEmailStage(TestCase): session.save() url = reverse( - "authentik_flows:flow-executor", kwargs={"flow_slug": self.flow.slug} + "authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug} ) with self.settings( EMAIL_BACKEND="django.core.mail.backends.locmem.EmailBackend" @@ -118,7 +118,7 @@ class TestEmailStage(TestCase): # Call the actual executor to get the JSON Response response = self.client.get( reverse( - "authentik_flows:flow-executor", + "authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}, ) ) diff --git a/authentik/stages/identification/stage.py b/authentik/stages/identification/stage.py index 949b127437..7c9de32e04 100644 --- a/authentik/stages/identification/stage.py +++ b/authentik/stages/identification/stage.py @@ -7,64 +7,33 @@ from django.http import HttpResponse from django.shortcuts import reverse from django.utils.translation import gettext as _ from django.views.generic import FormView +from rest_framework.fields import CharField from structlog.stdlib import get_logger from authentik.core.models import Source, User +from authentik.core.types import UILoginButton +from authentik.flows.challenge import Challenge, ChallengeResponse, ChallengeTypes from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER -from authentik.flows.stage import PLAN_CONTEXT_PENDING_USER_IDENTIFIER, StageView +from authentik.flows.stage import ( + PLAN_CONTEXT_PENDING_USER_IDENTIFIER, + ChallengeStageView, + StageView, +) from authentik.flows.views import SESSION_KEY_APPLICATION_PRE from authentik.stages.identification.forms import IdentificationForm -from authentik.stages.identification.models import IdentificationStage +from authentik.stages.identification.models import IdentificationStage, UserFields LOGGER = get_logger() -class IdentificationStageView(FormView, StageView): +class IdentificationChallengeResponse(ChallengeResponse): + + uid_field = CharField() + + +class IdentificationStageView(ChallengeStageView): """Form to identify the user""" - form_class = IdentificationForm - - def get_form_kwargs(self): - kwargs = super().get_form_kwargs() - kwargs["stage"] = self.executor.current_stage - return kwargs - - def get_template_names(self) -> List[str]: - current_stage: IdentificationStage = self.executor.current_stage - return [current_stage.template] - - def get_context_data(self, **kwargs): - current_stage: IdentificationStage = self.executor.current_stage - # If the user has been redirected to us whilst trying to access an - # application, SESSION_KEY_APPLICATION_PRE is set in the session - if SESSION_KEY_APPLICATION_PRE in self.request.session: - kwargs["application_pre"] = self.request.session[ - SESSION_KEY_APPLICATION_PRE - ] - # Check for related enrollment and recovery flow, add URL to view - if current_stage.enrollment_flow: - kwargs["enroll_url"] = reverse( - "authentik_flows:flow-executor-shell", - kwargs={"flow_slug": current_stage.enrollment_flow.slug}, - ) - if current_stage.recovery_flow: - kwargs["recovery_url"] = reverse( - "authentik_flows:flow-executor-shell", - kwargs={"flow_slug": current_stage.recovery_flow.slug}, - ) - kwargs["primary_action"] = _("Log in") - - # Check all enabled source, add them if they have a UI Login button. - kwargs["sources"] = [] - sources: List[Source] = ( - Source.objects.filter(enabled=True).order_by("name").select_subclasses() - ) - for source in sources: - ui_login_button = source.ui_login_button - if ui_login_button: - kwargs["sources"].append(ui_login_button) - return super().get_context_data(**kwargs) - def get_user(self, uid_value: str) -> Optional[User]: """Find user instance. Returns None if no user was found.""" current_stage: IdentificationStage = self.executor.current_stage @@ -82,14 +51,54 @@ class IdentificationStageView(FormView, StageView): return users.first() return None - def form_valid(self, form: IdentificationForm) -> HttpResponse: - """Form data is valid""" - user_identifier = form.cleaned_data.get("uid_field") + def get_challenge(self) -> Challenge: + current_stage: IdentificationStage = self.executor.current_stage + args = {"input_type": "text"} + if current_stage.user_fields == [UserFields.E_MAIL]: + args["input_type"] = "email" + # If the user has been redirected to us whilst trying to access an + # application, SESSION_KEY_APPLICATION_PRE is set in the session + if SESSION_KEY_APPLICATION_PRE in self.request.session: + args["application_pre"] = self.request.session[SESSION_KEY_APPLICATION_PRE] + # Check for related enrollment and recovery flow, add URL to view + if current_stage.enrollment_flow: + args["enroll_url"] = reverse( + "authentik_flows:flow-executor-shell", + args={"flow_slug": current_stage.enrollment_flow.slug}, + ) + if current_stage.recovery_flow: + args["recovery_url"] = reverse( + "authentik_flows:flow-executor-shell", + args={"flow_slug": current_stage.recovery_flow.slug}, + ) + args["primary_action"] = _("Log in") + + # Check all enabled source, add them if they have a UI Login button. + args["sources"] = [] + sources: List[Source] = ( + Source.objects.filter(enabled=True).order_by("name").select_subclasses() + ) + for source in sources: + ui_login_button = source.ui_login_button + if ui_login_button: + args["sources"].append(ui_login_button) + return Challenge( + data={ + "type": ChallengeTypes.native, + "component": "ak-stage-identification", + "args": args, + } + ) + + def challenge_valid( + self, challenge: IdentificationChallengeResponse + ) -> HttpResponse: + user_identifier = challenge.data.get("uid_field") pre_user = self.get_user(user_identifier) if not pre_user: LOGGER.debug("invalid_login") messages.error(self.request, _("Failed to authenticate.")) - return self.form_invalid(form) + return self.challenge_invalid(challenge) self.executor.plan.context[PLAN_CONTEXT_PENDING_USER] = pre_user current_stage: IdentificationStage = self.executor.current_stage diff --git a/authentik/stages/identification/tests.py b/authentik/stages/identification/tests.py index 326256e476..d3b4517156 100644 --- a/authentik/stages/identification/tests.py +++ b/authentik/stages/identification/tests.py @@ -43,9 +43,7 @@ class TestIdentificationStage(TestCase): def test_valid_render(self): """Test that View renders correctly""" response = self.client.get( - reverse( - "authentik_flows:flow-executor", kwargs={"flow_slug": self.flow.slug} - ) + reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}) ) self.assertEqual(response.status_code, 200) @@ -53,7 +51,7 @@ class TestIdentificationStage(TestCase): """Test with valid email, check that URL redirects back to itself""" form_data = {"uid_field": self.user.email} url = reverse( - "authentik_flows:flow-executor", kwargs={"flow_slug": self.flow.slug} + "authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug} ) response = self.client.post(url, form_data) self.assertEqual(response.status_code, 200) @@ -67,7 +65,7 @@ class TestIdentificationStage(TestCase): form_data = {"uid_field": self.user.username} response = self.client.post( reverse( - "authentik_flows:flow-executor", kwargs={"flow_slug": self.flow.slug} + "authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug} ), form_data, ) @@ -78,7 +76,7 @@ class TestIdentificationStage(TestCase): form_data = {"uid_field": self.user.email + "test"} response = self.client.post( reverse( - "authentik_flows:flow-executor", kwargs={"flow_slug": self.flow.slug} + "authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug} ), form_data, ) @@ -101,7 +99,7 @@ class TestIdentificationStage(TestCase): response = self.client.get( reverse( - "authentik_flows:flow-executor", kwargs={"flow_slug": self.flow.slug} + "authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug} ), ) self.assertEqual(response.status_code, 200) @@ -124,7 +122,7 @@ class TestIdentificationStage(TestCase): response = self.client.get( reverse( - "authentik_flows:flow-executor", kwargs={"flow_slug": self.flow.slug} + "authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug} ), ) self.assertEqual(response.status_code, 200) diff --git a/authentik/stages/invitation/tests.py b/authentik/stages/invitation/tests.py index 94f6d3d616..072ecd49e8 100644 --- a/authentik/stages/invitation/tests.py +++ b/authentik/stages/invitation/tests.py @@ -58,9 +58,7 @@ class TestUserLoginStage(TestCase): session.save() response = self.client.get( - reverse( - "authentik_flows:flow-executor", kwargs={"flow_slug": self.flow.slug} - ) + reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}) ) self.assertEqual(response.status_code, 200) self.assertIsInstance(response, AccessDeniedResponse) @@ -81,9 +79,7 @@ class TestUserLoginStage(TestCase): session.save() response = self.client.get( - reverse( - "authentik_flows:flow-executor", kwargs={"flow_slug": self.flow.slug} - ) + reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}) ) self.assertEqual(response.status_code, 200) @@ -115,7 +111,7 @@ class TestUserLoginStage(TestCase): with patch("authentik.flows.views.FlowExecutorView.cancel", MagicMock()): base_url = reverse( - "authentik_flows:flow-executor", kwargs={"flow_slug": self.flow.slug} + "authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug} ) response = self.client.get( base_url + f"?{INVITATION_TOKEN_KEY}={invite.pk.hex}" diff --git a/authentik/stages/password/templates/stages/password/flow-form.html b/authentik/stages/password/templates/stages/password/flow-form.html index b0cece8d66..21ea7e4766 100644 --- a/authentik/stages/password/templates/stages/password/flow-form.html +++ b/authentik/stages/password/templates/stages/password/flow-form.html @@ -5,6 +5,6 @@ {% block beneath_form %} {% if recovery_flow %} -{% trans 'Forgot password?' %} +{% trans 'Forgot password?' %} {% endif %} {% endblock %} diff --git a/authentik/stages/password/tests.py b/authentik/stages/password/tests.py index ed604f45c3..0d7c35a5cb 100644 --- a/authentik/stages/password/tests.py +++ b/authentik/stages/password/tests.py @@ -59,7 +59,7 @@ class TestPasswordStage(TestCase): response = self.client.post( reverse( - "authentik_flows:flow-executor", kwargs={"flow_slug": self.flow.slug} + "authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug} ), # Still have to send the password so the form is valid {"password": self.password}, @@ -83,7 +83,7 @@ class TestPasswordStage(TestCase): response = self.client.get( reverse( - "authentik_flows:flow-executor", kwargs={"flow_slug": self.flow.slug} + "authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug} ), ) self.assertEqual(response.status_code, 200) @@ -101,7 +101,7 @@ class TestPasswordStage(TestCase): response = self.client.post( reverse( - "authentik_flows:flow-executor", kwargs={"flow_slug": self.flow.slug} + "authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug} ), # Form data {"password": self.password}, @@ -125,7 +125,7 @@ class TestPasswordStage(TestCase): response = self.client.post( reverse( - "authentik_flows:flow-executor", kwargs={"flow_slug": self.flow.slug} + "authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug} ), # Form data {"password": self.password + "test"}, @@ -145,7 +145,7 @@ class TestPasswordStage(TestCase): for _ in range(self.stage.failed_attempts_before_cancel): response = self.client.post( reverse( - "authentik_flows:flow-executor", + "authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}, ), # Form data @@ -155,7 +155,7 @@ class TestPasswordStage(TestCase): response = self.client.post( reverse( - "authentik_flows:flow-executor", kwargs={"flow_slug": self.flow.slug} + "authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug} ), # Form data {"password": self.password + "test"}, @@ -185,7 +185,7 @@ class TestPasswordStage(TestCase): response = self.client.post( reverse( - "authentik_flows:flow-executor", kwargs={"flow_slug": self.flow.slug} + "authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug} ), # Form data {"password": self.password + "test"}, diff --git a/authentik/stages/prompt/tests.py b/authentik/stages/prompt/tests.py index bbacd8c26d..b2cbb95c39 100644 --- a/authentik/stages/prompt/tests.py +++ b/authentik/stages/prompt/tests.py @@ -104,9 +104,7 @@ class TestPromptStage(TestCase): session.save() response = self.client.get( - reverse( - "authentik_flows:flow-executor", kwargs={"flow_slug": self.flow.slug} - ) + reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}) ) self.assertEqual(response.status_code, 200) for prompt in self.stage.fields.all(): @@ -158,7 +156,7 @@ class TestPromptStage(TestCase): with patch("authentik.flows.views.FlowExecutorView.cancel", MagicMock()): response = self.client.post( reverse( - "authentik_flows:flow-executor", + "authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}, ), form.cleaned_data, diff --git a/authentik/stages/user_delete/tests.py b/authentik/stages/user_delete/tests.py index fb87118bcd..85c6a7c09d 100644 --- a/authentik/stages/user_delete/tests.py +++ b/authentik/stages/user_delete/tests.py @@ -46,9 +46,7 @@ class TestUserDeleteStage(TestCase): session.save() response = self.client.get( - reverse( - "authentik_flows:flow-executor", kwargs={"flow_slug": self.flow.slug} - ) + reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}) ) self.assertEqual(response.status_code, 200) self.assertIsInstance(response, AccessDeniedResponse) @@ -64,9 +62,7 @@ class TestUserDeleteStage(TestCase): session.save() response = self.client.get( - reverse( - "authentik_flows:flow-executor", kwargs={"flow_slug": self.flow.slug} - ) + reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}) ) self.assertEqual(response.status_code, 200) @@ -82,7 +78,7 @@ class TestUserDeleteStage(TestCase): response = self.client.post( reverse( - "authentik_flows:flow-executor", kwargs={"flow_slug": self.flow.slug} + "authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug} ), {}, ) diff --git a/authentik/stages/user_login/tests.py b/authentik/stages/user_login/tests.py index 059793f8b9..7a5a296791 100644 --- a/authentik/stages/user_login/tests.py +++ b/authentik/stages/user_login/tests.py @@ -47,9 +47,7 @@ class TestUserLoginStage(TestCase): session.save() response = self.client.get( - reverse( - "authentik_flows:flow-executor", kwargs={"flow_slug": self.flow.slug} - ) + reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}) ) self.assertEqual(response.status_code, 200) @@ -72,9 +70,7 @@ class TestUserLoginStage(TestCase): session.save() response = self.client.get( - reverse( - "authentik_flows:flow-executor", kwargs={"flow_slug": self.flow.slug} - ) + reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}) ) self.assertEqual(response.status_code, 200) @@ -95,9 +91,7 @@ class TestUserLoginStage(TestCase): session.save() response = self.client.get( - reverse( - "authentik_flows:flow-executor", kwargs={"flow_slug": self.flow.slug} - ) + reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}) ) self.assertEqual(response.status_code, 200) diff --git a/authentik/stages/user_logout/tests.py b/authentik/stages/user_logout/tests.py index dd3b9367d2..f50cf95f1e 100644 --- a/authentik/stages/user_logout/tests.py +++ b/authentik/stages/user_logout/tests.py @@ -43,9 +43,7 @@ class TestUserLogoutStage(TestCase): session.save() response = self.client.get( - reverse( - "authentik_flows:flow-executor", kwargs={"flow_slug": self.flow.slug} - ) + reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}) ) self.assertEqual(response.status_code, 200) diff --git a/authentik/stages/user_write/tests.py b/authentik/stages/user_write/tests.py index 214c693c73..52f9f8f13d 100644 --- a/authentik/stages/user_write/tests.py +++ b/authentik/stages/user_write/tests.py @@ -55,9 +55,7 @@ class TestUserWriteStage(TestCase): session.save() response = self.client.get( - reverse( - "authentik_flows:flow-executor", kwargs={"flow_slug": self.flow.slug} - ) + reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}) ) self.assertEqual(response.status_code, 200) @@ -94,9 +92,7 @@ class TestUserWriteStage(TestCase): session.save() response = self.client.get( - reverse( - "authentik_flows:flow-executor", kwargs={"flow_slug": self.flow.slug} - ) + reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}) ) self.assertEqual(response.status_code, 200) @@ -126,9 +122,7 @@ class TestUserWriteStage(TestCase): session.save() response = self.client.get( - reverse( - "authentik_flows:flow-executor", kwargs={"flow_slug": self.flow.slug} - ) + reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}) ) self.assertEqual(response.status_code, 200) diff --git a/web/rollup.config.js b/web/rollup.config.js index 3e366b3f1a..c818414bce 100644 --- a/web/rollup.config.js +++ b/web/rollup.config.js @@ -48,4 +48,33 @@ export default [ }, external: ["django"] }, + { + input: "./src/flow.ts", + output: [ + { + format: "es", + dir: "dist", + sourcemap: true, + }, + ], + plugins: [ + cssimport(), + typescript(), + externalGlobals({ + django: "django" + }), + resolve({ browser: true }), + commonjs(), + sourcemaps(), + terser(), + copy({ + targets: [...resources], + copyOnce: false, + }), + ], + watch: { + clearScreen: false, + }, + external: ["django"] + }, ]; diff --git a/web/src/elements/stages/authenticator_validate/AuthenticatorValidateStage.ts b/web/src/elements/stages/authenticator_validate/AuthenticatorValidateStage.ts new file mode 100644 index 0000000000..ce9ba9a82e --- /dev/null +++ b/web/src/elements/stages/authenticator_validate/AuthenticatorValidateStage.ts @@ -0,0 +1,10 @@ +import { customElement, html, LitElement, TemplateResult } from "lit-element"; + +@customElement("ak-stage-authenticator-validate") +export class AuthenticatorValidateStage extends LitElement { + + render(): TemplateResult { + return html`ak-stage-authenticator-validate`; + } + +} diff --git a/web/src/elements/stages/base.ts b/web/src/elements/stages/base.ts new file mode 100644 index 0000000000..d2cf896681 --- /dev/null +++ b/web/src/elements/stages/base.ts @@ -0,0 +1,10 @@ +import { LitElement } from "lit-element"; +import { FlowExecutor } from "../../pages/generic/FlowExecutor"; + +export class BaseStage extends LitElement { + + // submit() + + host?: FlowExecutor; + +} diff --git a/web/src/elements/stages/identification/IdentificationStage.ts b/web/src/elements/stages/identification/IdentificationStage.ts new file mode 100644 index 0000000000..08eaa50c1f --- /dev/null +++ b/web/src/elements/stages/identification/IdentificationStage.ts @@ -0,0 +1,101 @@ +import { gettext } from "django"; +import { CSSResult, customElement, html, property, TemplateResult } from "lit-element"; +import { COMMON_STYLES } from "../../../common/styles"; +import { BaseStage } from "../base"; + +export interface IdentificationStageArgs { + + input_type: string; + primary_action: string; + sources: string[]; + + application_pre?: string; + +} + +@customElement("ak-stage-identification") +export class IdentificationStage extends BaseStage { + + @property({attribute: false}) + args?: IdentificationStageArgs; + + static get styles(): CSSResult[] { + return COMMON_STYLES; + } + + render(): TemplateResult { + if (!this.args) { + return html``; + } + return html` + + `; + } + +} diff --git a/web/src/flow.ts b/web/src/flow.ts new file mode 100644 index 0000000000..202eaae2fb --- /dev/null +++ b/web/src/flow.ts @@ -0,0 +1,3 @@ +import "construct-style-sheets-polyfill"; + +import "./pages/generic/FlowExecutor"; diff --git a/web/src/main.ts b/web/src/main.ts index 9922f7ff08..d3b3d742f5 100644 --- a/web/src/main.ts +++ b/web/src/main.ts @@ -22,7 +22,6 @@ import "./elements/Spinner"; import "./elements/Tabs"; import "./elements/router/RouterOutlet"; -import "./pages/generic/FlowShellCard"; import "./pages/generic/SiteShell"; import "./pages/admin-overview/AdminOverviewPage"; @@ -33,5 +32,7 @@ import "./pages/LibraryPage"; import "./elements/stages/authenticator_webauthn/WebAuthnRegister"; import "./elements/stages/authenticator_webauthn/WebAuthnAuth"; +import "./elements/stages/authenticator_validate/AuthenticatorValidateStage"; +import "./elements/stages/identification/IdentificationStage"; import "./interfaces/AdminInterface"; diff --git a/web/src/pages/flows/BoundStagesList.ts b/web/src/pages/flows/BoundStagesList.ts index 464ef47a47..a1b95c3dba 100644 --- a/web/src/pages/flows/BoundStagesList.ts +++ b/web/src/pages/flows/BoundStagesList.ts @@ -7,6 +7,7 @@ import "../../elements/Tabs"; import "../../elements/AdminLoginsChart"; import "../../elements/buttons/ModalButton"; import "../../elements/buttons/SpinnerButton"; +import "../../elements/buttons/Dropdown"; import "../../elements/policies/BoundPoliciesList"; import { FlowStageBinding, Stage } from "../../api/Flows"; import { until } from "lit-html/directives/until"; diff --git a/web/src/pages/generic/FlowShellCard.ts b/web/src/pages/generic/FlowExecutor.ts similarity index 68% rename from web/src/pages/generic/FlowShellCard.ts rename to web/src/pages/generic/FlowExecutor.ts index 3131758350..61e51005ff 100644 --- a/web/src/pages/generic/FlowShellCard.ts +++ b/web/src/pages/generic/FlowExecutor.ts @@ -1,25 +1,30 @@ +import { gettext } from "django"; import { LitElement, html, customElement, property, TemplateResult } from "lit-element"; +import { unsafeHTML } from "lit-html/directives/unsafe-html"; import { SentryIgnoredError } from "../../common/errors"; import { getCookie } from "../../utils"; +import "../../elements/stages/identification/IdentificationStage"; -enum ResponseType { +enum ChallengeTypes { + native = "native", + response = "response", + shell = "shell", redirect = "redirect", - template = "template", } -interface Response { - type: ResponseType; - to?: string; - body?: string; +interface Challenge { + type: ChallengeTypes; + args: { [key: string]: string }; + component: string; } -@customElement("ak-flow-shell-card") -export class FlowShellCard extends LitElement { +@customElement("ak-flow-executor") +export class FlowExecutor extends LitElement { @property() flowBodyUrl = ""; - @property() - flowBody?: string; + @property({attribute: false}) + flowBody?: TemplateResult; createRenderRoot(): Element | ShadowRoot { return this; @@ -28,28 +33,33 @@ export class FlowShellCard extends LitElement { constructor() { super(); this.addEventListener("ak-flow-submit", () => { - const csrftoken = getCookie("authentik_csrf"); - const request = new Request(this.flowBodyUrl, { - headers: { - "X-CSRFToken": csrftoken, - }, - }); - fetch(request, { - method: "POST", - mode: "same-origin" - }) - .then((response) => { - return response.json(); - }) - .then((data) => { - this.updateCard(data); - }) - .catch((e) => { - this.errorMessage(e); - }); + this.submit(); }); } + submit(formData?: FormData): void { + const csrftoken = getCookie("authentik_csrf"); + const request = new Request(this.flowBodyUrl, { + headers: { + "X-CSRFToken": csrftoken, + }, + }); + fetch(request, { + method: "POST", + mode: "same-origin", + body: formData, + }) + .then((response) => { + return response.json(); + }) + .then((data) => { + this.updateCard(data); + }) + .catch((e) => { + this.errorMessage(e); + }); + } + firstUpdated(): void { fetch(this.flowBodyUrl) .then((r) => { @@ -73,19 +83,29 @@ export class FlowShellCard extends LitElement { }); } - async updateCard(data: Response): Promise { + async updateCard(data: Challenge): Promise { switch (data.type) { - case ResponseType.redirect: - console.debug(`authentik/flows: redirecting to ${data.to}`); - window.location.assign(data.to || ""); + case ChallengeTypes.redirect: + console.debug(`authentik/flows: redirecting to ${data.args.to}`); + window.location.assign(data.args.to || ""); break; - case ResponseType.template: - this.flowBody = data.body; + case ChallengeTypes.shell: + this.flowBody = html`${unsafeHTML(data.args.body)}`; await this.requestUpdate(); this.checkAutofocus(); this.loadFormCode(); this.setFormSubmitHandlers(); break; + case ChallengeTypes.native: + switch (data.component) { + case "ak-stage-identification": + this.flowBody = html``; + break; + default: + break; + } + // this.flowBody = html`${unsafeHTML(`<${data.component} .args="${data.args}">`)}`; + break; default: console.debug(`authentik/flows: unexpected data type ${data.type}`); break; @@ -139,26 +159,14 @@ export class FlowShellCard extends LitElement { e.preventDefault(); const formData = new FormData(form); this.flowBody = undefined; - fetch(this.flowBodyUrl, { - method: "post", - body: formData, - }) - .then((response) => { - return response.json(); - }) - .then((data) => { - this.updateCard(data); - }) - .catch((e) => { - this.errorMessage(e); - }); + this.submit(formData); }); form.classList.add("ak-flow-wrapped"); }); } errorMessage(error: string): void { - this.flowBody = ` + this.flowBody = html` `; } @@ -190,7 +196,7 @@ export class FlowShellCard extends LitElement { render(): TemplateResult { if (this.flowBody) { - return html(([this.flowBody])); + return this.flowBody; } return this.loading(); }