flows: mount executor under api, implement initial challenge design
This commit is contained in:
parent
8708e487ae
commit
eb01b42425
|
@ -29,6 +29,7 @@ from authentik.flows.api import (
|
||||||
FlowViewSet,
|
FlowViewSet,
|
||||||
StageViewSet,
|
StageViewSet,
|
||||||
)
|
)
|
||||||
|
from authentik.flows.views import FlowExecutorView
|
||||||
from authentik.outposts.api.outpost_service_connections import (
|
from authentik.outposts.api.outpost_service_connections import (
|
||||||
DockerServiceConnectionViewSet,
|
DockerServiceConnectionViewSet,
|
||||||
KubernetesServiceConnectionViewSet,
|
KubernetesServiceConnectionViewSet,
|
||||||
|
@ -184,4 +185,9 @@ urlpatterns = [
|
||||||
name="schema-swagger-ui",
|
name="schema-swagger-ui",
|
||||||
),
|
),
|
||||||
path("redoc/", SchemaView.with_ui("redoc", cache_timeout=0), name="schema-redoc"),
|
path("redoc/", SchemaView.with_ui("redoc", cache_timeout=0), name="schema-redoc"),
|
||||||
|
path(
|
||||||
|
"flows/executor/<slug:flow_slug>/",
|
||||||
|
FlowExecutorView.as_view(),
|
||||||
|
name="flow-executor",
|
||||||
|
),
|
||||||
] + router.urls
|
] + router.urls
|
||||||
|
|
|
@ -16,7 +16,6 @@
|
||||||
<link rel="stylesheet" type="text/css" href="{% static 'dist/fontawesome.min.css' %}?v={{ ak_version }}">
|
<link rel="stylesheet" type="text/css" href="{% static 'dist/fontawesome.min.css' %}?v={{ ak_version }}">
|
||||||
<link rel="stylesheet" type="text/css" href="{% static 'dist/authentik.css' %}?v={{ ak_version }}">
|
<link rel="stylesheet" type="text/css" href="{% static 'dist/authentik.css' %}?v={{ ak_version }}">
|
||||||
<script src="{% url 'javascript-catalog' %}?v={{ ak_version }}"></script>
|
<script src="{% url 'javascript-catalog' %}?v={{ ak_version }}"></script>
|
||||||
<script src="{% static 'dist/main.js' %}?v={{ ak_version }}" type="module"></script>
|
|
||||||
{% block head %}
|
{% block head %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
</head>
|
</head>
|
||||||
|
|
|
@ -1,5 +1,9 @@
|
||||||
{% extends "base/skeleton.html" %}
|
{% extends "base/skeleton.html" %}
|
||||||
|
|
||||||
|
{% block head %}
|
||||||
|
<script src="{% static 'dist/main.js' %}?v={{ ak_version }}" type="module"></script>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
<ak-interface-admin></ak-interface-admin>
|
<ak-interface-admin></ak-interface-admin>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
37
authentik/flows/challenge.py
Normal file
37
authentik/flows/challenge.py
Normal file
|
@ -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)
|
|
@ -3,9 +3,15 @@ from collections import namedtuple
|
||||||
from typing import Any, Dict
|
from typing import Any, Dict
|
||||||
|
|
||||||
from django.http import HttpRequest
|
from django.http import HttpRequest
|
||||||
|
from django.http.response import HttpResponse, JsonResponse
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django.views.generic import TemplateView
|
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.planner import PLAN_CONTEXT_PENDING_USER
|
||||||
from authentik.flows.views import FlowExecutorView
|
from authentik.flows.views import FlowExecutorView
|
||||||
|
|
||||||
|
@ -43,3 +49,28 @@ class StageView(TemplateView):
|
||||||
)
|
)
|
||||||
kwargs["primary_action"] = _("Continue")
|
kwargs["primary_action"] = _("Continue")
|
||||||
return super().get_context_data(**kwargs)
|
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)
|
||||||
|
|
|
@ -22,11 +22,10 @@
|
||||||
background-position: center;
|
background-position: center;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
<script src="{% static 'dist/flow.js' %}?v={{ ak_version }}" type="module"></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block main_container %}
|
{% block main_container %}
|
||||||
<ak-flow-shell-card
|
<ak-flow-executor class="pf-c-login__main" flowBodyUrl="{{ exec_url }}">
|
||||||
class="pf-c-login__main"
|
</ak-flow-executor>
|
||||||
flowBodyUrl="{{ exec_url }}">
|
|
||||||
</ak-flow-shell-card>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -43,7 +43,7 @@ class TestFlowPlanner(TestCase):
|
||||||
designation=FlowDesignation.AUTHENTICATION,
|
designation=FlowDesignation.AUTHENTICATION,
|
||||||
)
|
)
|
||||||
request = self.request_factory.get(
|
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()
|
request.user = get_anonymous_user()
|
||||||
|
|
||||||
|
@ -63,7 +63,7 @@ class TestFlowPlanner(TestCase):
|
||||||
designation=FlowDesignation.AUTHENTICATION,
|
designation=FlowDesignation.AUTHENTICATION,
|
||||||
)
|
)
|
||||||
request = self.request_factory.get(
|
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()
|
request.user = get_anonymous_user()
|
||||||
|
|
||||||
|
@ -83,7 +83,7 @@ class TestFlowPlanner(TestCase):
|
||||||
target=flow, stage=DummyStage.objects.create(name="dummy"), order=0
|
target=flow, stage=DummyStage.objects.create(name="dummy"), order=0
|
||||||
)
|
)
|
||||||
request = self.request_factory.get(
|
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()
|
request.user = get_anonymous_user()
|
||||||
|
|
||||||
|
@ -112,7 +112,7 @@ class TestFlowPlanner(TestCase):
|
||||||
|
|
||||||
user = User.objects.create(username="test-user")
|
user = User.objects.create(username="test-user")
|
||||||
request = self.request_factory.get(
|
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
|
request.user = user
|
||||||
planner = FlowPlanner(flow)
|
planner = FlowPlanner(flow)
|
||||||
|
@ -136,7 +136,7 @@ class TestFlowPlanner(TestCase):
|
||||||
)
|
)
|
||||||
|
|
||||||
request = self.request_factory.get(
|
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()
|
request.user = get_anonymous_user()
|
||||||
|
|
||||||
|
@ -167,7 +167,7 @@ class TestFlowPlanner(TestCase):
|
||||||
PolicyBinding.objects.create(policy=false_policy, target=binding2, order=0)
|
PolicyBinding.objects.create(policy=false_policy, target=binding2, order=0)
|
||||||
|
|
||||||
request = self.request_factory.get(
|
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()
|
request.user = get_anonymous_user()
|
||||||
|
|
||||||
|
|
|
@ -62,9 +62,7 @@ class TestFlowExecutor(TestCase):
|
||||||
cancel_mock = MagicMock()
|
cancel_mock = MagicMock()
|
||||||
with patch("authentik.flows.views.FlowExecutorView.cancel", cancel_mock):
|
with patch("authentik.flows.views.FlowExecutorView.cancel", cancel_mock):
|
||||||
response = self.client.get(
|
response = self.client.get(
|
||||||
reverse(
|
reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}),
|
||||||
"authentik_flows:flow-executor", kwargs={"flow_slug": flow.slug}
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
self.assertEqual(cancel_mock.call_count, 2)
|
self.assertEqual(cancel_mock.call_count, 2)
|
||||||
|
@ -87,7 +85,7 @@ class TestFlowExecutor(TestCase):
|
||||||
|
|
||||||
CONFIG.update_from_dict({"domain": "testserver"})
|
CONFIG.update_from_dict({"domain": "testserver"})
|
||||||
response = self.client.get(
|
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.assertEqual(response.status_code, 200)
|
||||||
self.assertIsInstance(response, AccessDeniedResponse)
|
self.assertIsInstance(response, AccessDeniedResponse)
|
||||||
|
@ -107,7 +105,7 @@ class TestFlowExecutor(TestCase):
|
||||||
|
|
||||||
CONFIG.update_from_dict({"domain": "testserver"})
|
CONFIG.update_from_dict({"domain": "testserver"})
|
||||||
response = self.client.get(
|
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.status_code, 302)
|
||||||
self.assertEqual(response.url, reverse("authentik_core:shell"))
|
self.assertEqual(response.url, reverse("authentik_core:shell"))
|
||||||
|
@ -126,7 +124,7 @@ class TestFlowExecutor(TestCase):
|
||||||
|
|
||||||
CONFIG.update_from_dict({"domain": "testserver"})
|
CONFIG.update_from_dict({"domain": "testserver"})
|
||||||
dest = "/unique-string"
|
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}")
|
response = self.client.get(url + f"?{NEXT_ARG_NAME}={dest}")
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
self.assertEqual(response.url, reverse("authentik_core:shell"))
|
self.assertEqual(response.url, reverse("authentik_core:shell"))
|
||||||
|
@ -146,7 +144,7 @@ class TestFlowExecutor(TestCase):
|
||||||
)
|
)
|
||||||
|
|
||||||
exec_url = reverse(
|
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
|
# First Request, start planning, renders form
|
||||||
response = self.client.get(exec_url)
|
response = self.client.get(exec_url)
|
||||||
|
@ -196,7 +194,7 @@ class TestFlowExecutor(TestCase):
|
||||||
):
|
):
|
||||||
|
|
||||||
exec_url = reverse(
|
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
|
# First request, run the planner
|
||||||
response = self.client.get(exec_url)
|
response = self.client.get(exec_url)
|
||||||
|
@ -250,7 +248,7 @@ class TestFlowExecutor(TestCase):
|
||||||
):
|
):
|
||||||
|
|
||||||
exec_url = reverse(
|
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
|
# First request, run the planner
|
||||||
response = self.client.get(exec_url)
|
response = self.client.get(exec_url)
|
||||||
|
@ -317,7 +315,7 @@ class TestFlowExecutor(TestCase):
|
||||||
):
|
):
|
||||||
|
|
||||||
exec_url = reverse(
|
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
|
# First request, run the planner
|
||||||
response = self.client.get(exec_url)
|
response = self.client.get(exec_url)
|
||||||
|
@ -401,7 +399,7 @@ class TestFlowExecutor(TestCase):
|
||||||
):
|
):
|
||||||
|
|
||||||
exec_url = reverse(
|
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
|
# First request, run the planner
|
||||||
response = self.client.get(exec_url)
|
response = self.client.get(exec_url)
|
||||||
|
@ -455,7 +453,7 @@ class TestFlowExecutor(TestCase):
|
||||||
|
|
||||||
user = User.objects.create(username="test-user")
|
user = User.objects.create(username="test-user")
|
||||||
request = self.request_factory.get(
|
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
|
request.user = user
|
||||||
planner = FlowPlanner(flow)
|
planner = FlowPlanner(flow)
|
||||||
|
|
|
@ -19,6 +19,7 @@ from structlog.stdlib import BoundLogger, get_logger
|
||||||
|
|
||||||
from authentik.core.models import USER_ATTRIBUTE_DEBUG
|
from authentik.core.models import USER_ATTRIBUTE_DEBUG
|
||||||
from authentik.events.models import cleanse_dict
|
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.exceptions import EmptyFlowException, FlowNonApplicableException
|
||||||
from authentik.flows.models import ConfigurableStage, Flow, FlowDesignation, Stage
|
from authentik.flows.models import ConfigurableStage, Flow, FlowDesignation, Stage
|
||||||
from authentik.flows.planner import (
|
from authentik.flows.planner import (
|
||||||
|
@ -176,7 +177,7 @@ class FlowExecutorView(View):
|
||||||
reamining=len(self.plan.stages),
|
reamining=len(self.plan.stages),
|
||||||
)
|
)
|
||||||
return redirect_with_qs(
|
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
|
# User passed all stages
|
||||||
self._logger.debug(
|
self._logger.debug(
|
||||||
|
@ -246,9 +247,7 @@ class FlowExecutorShellView(TemplateView):
|
||||||
def get_context_data(self, **kwargs) -> Dict[str, Any]:
|
def get_context_data(self, **kwargs) -> Dict[str, Any]:
|
||||||
flow: Flow = get_object_or_404(Flow, slug=self.kwargs.get("flow_slug"))
|
flow: Flow = get_object_or_404(Flow, slug=self.kwargs.get("flow_slug"))
|
||||||
kwargs["background_url"] = flow.background.url
|
kwargs["background_url"] = flow.background.url
|
||||||
kwargs["exec_url"] = reverse(
|
kwargs["exec_url"] = reverse("authentik_api:flow-executor", kwargs=self.kwargs)
|
||||||
"authentik_flows:flow-executor", kwargs=self.kwargs
|
|
||||||
)
|
|
||||||
self.request.session[SESSION_KEY_GET] = self.request.GET
|
self.request.session[SESSION_KEY_GET] = self.request.GET
|
||||||
return kwargs
|
return kwargs
|
||||||
|
|
||||||
|
@ -292,17 +291,38 @@ def to_stage_response(request: HttpRequest, source: HttpResponse) -> HttpRespons
|
||||||
if isinstance(source, HttpResponseRedirect) or source.status_code == 302:
|
if isinstance(source, HttpResponseRedirect) or source.status_code == 302:
|
||||||
redirect_url = source["Location"]
|
redirect_url = source["Location"]
|
||||||
if request.path != redirect_url:
|
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
|
return source
|
||||||
if isinstance(source, TemplateResponse):
|
if isinstance(source, TemplateResponse):
|
||||||
return JsonResponse(
|
return HttpChallengeResponse(
|
||||||
{"type": "template", "body": source.render().content.decode("utf-8")}
|
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)
|
# Check for actual HttpResponse (without isinstance as we dont want to check inheritance)
|
||||||
if source.__class__ == HttpResponse:
|
if source.__class__ == HttpResponse:
|
||||||
return JsonResponse(
|
return HttpChallengeResponse(
|
||||||
{"type": "template", "body": source.content.decode("utf-8")}
|
Challenge(
|
||||||
|
{
|
||||||
|
"type": ChallengeTypes.shell,
|
||||||
|
"args": {"body": source.content.decode("utf-8")},
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
)
|
||||||
|
# return JsonResponse(
|
||||||
|
# {"type": "template", "body": }
|
||||||
|
# )
|
||||||
return source
|
return source
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -4,28 +4,37 @@ from typing import Any, Dict
|
||||||
from django.http import HttpRequest, HttpResponse
|
from django.http import HttpRequest, HttpResponse
|
||||||
from django.views.generic import FormView
|
from django.views.generic import FormView
|
||||||
from django_otp import user_has_device
|
from django_otp import user_has_device
|
||||||
|
from rest_framework.fields import IntegerField
|
||||||
from structlog.stdlib import get_logger
|
from structlog.stdlib import get_logger
|
||||||
|
|
||||||
|
from authentik.flows.challenge import Challenge, ChallengeResponse, ChallengeTypes
|
||||||
from authentik.flows.models import NotConfiguredAction
|
from authentik.flows.models import NotConfiguredAction
|
||||||
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER
|
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.forms import ValidationForm
|
||||||
from authentik.stages.authenticator_validate.models import AuthenticatorValidateStage
|
from authentik.stages.authenticator_validate.models import AuthenticatorValidateStage
|
||||||
|
|
||||||
LOGGER = get_logger()
|
LOGGER = get_logger()
|
||||||
|
|
||||||
|
|
||||||
class AuthenticatorValidateStageView(FormView, StageView):
|
class CodeChallengeResponse(ChallengeResponse):
|
||||||
|
|
||||||
|
code = IntegerField(min_value=0)
|
||||||
|
|
||||||
|
|
||||||
|
class WebAuthnChallengeResponse(ChallengeResponse):
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class AuthenticatorValidateStageView(ChallengeStageView):
|
||||||
"""OTP Validation"""
|
"""OTP Validation"""
|
||||||
|
|
||||||
form_class = ValidationForm
|
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:
|
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)
|
user = self.executor.plan.context.get(PLAN_CONTEXT_PENDING_USER)
|
||||||
if not user:
|
if not user:
|
||||||
LOGGER.debug("No pending user, continuing")
|
LOGGER.debug("No pending user, continuing")
|
||||||
|
@ -39,8 +48,27 @@ class AuthenticatorValidateStageView(FormView, StageView):
|
||||||
return self.executor.stage_ok()
|
return self.executor.stage_ok()
|
||||||
return super().get(request, *args, **kwargs)
|
return super().get(request, *args, **kwargs)
|
||||||
|
|
||||||
def form_valid(self, form: ValidationForm) -> HttpResponse:
|
# def get_form_kwargs(self, **kwargs) -> Dict[str, Any]:
|
||||||
"""Verify OTP Token"""
|
# kwargs = super().get_form_kwargs(**kwargs)
|
||||||
# Since we do token checking in the form, we know the token is valid here
|
# kwargs["user"] = self.executor.plan.context.get(PLAN_CONTEXT_PENDING_USER)
|
||||||
# so we can just continue
|
# return kwargs
|
||||||
return self.executor.stage_ok()
|
|
||||||
|
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()
|
||||||
|
|
|
@ -44,7 +44,7 @@ class TestCaptchaStage(TestCase):
|
||||||
session.save()
|
session.save()
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
reverse(
|
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"},
|
{"g-recaptcha-response": "PASSED"},
|
||||||
)
|
)
|
||||||
|
|
|
@ -45,7 +45,7 @@ class TestConsentStage(TestCase):
|
||||||
session[SESSION_KEY_PLAN] = plan
|
session[SESSION_KEY_PLAN] = plan
|
||||||
session.save()
|
session.save()
|
||||||
response = self.client.post(
|
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)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
@ -76,7 +76,7 @@ class TestConsentStage(TestCase):
|
||||||
session[SESSION_KEY_PLAN] = plan
|
session[SESSION_KEY_PLAN] = plan
|
||||||
session.save()
|
session.save()
|
||||||
response = self.client.post(
|
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)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
@ -113,7 +113,7 @@ class TestConsentStage(TestCase):
|
||||||
session[SESSION_KEY_PLAN] = plan
|
session[SESSION_KEY_PLAN] = plan
|
||||||
session.save()
|
session.save()
|
||||||
response = self.client.post(
|
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)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
|
@ -34,16 +34,14 @@ class TestDummyStage(TestCase):
|
||||||
def test_valid_render(self):
|
def test_valid_render(self):
|
||||||
"""Test that View renders correctly"""
|
"""Test that View renders correctly"""
|
||||||
response = self.client.get(
|
response = self.client.get(
|
||||||
reverse(
|
reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug})
|
||||||
"authentik_flows:flow-executor", kwargs={"flow_slug": self.flow.slug}
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
def test_post(self):
|
def test_post(self):
|
||||||
"""Test with valid email, check that URL redirects back to itself"""
|
"""Test with valid email, check that URL redirects back to itself"""
|
||||||
url = reverse(
|
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, {})
|
response = self.client.post(url, {})
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
|
@ -46,7 +46,7 @@ class TestEmailStageSending(TestCase):
|
||||||
session.save()
|
session.save()
|
||||||
|
|
||||||
url = reverse(
|
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(
|
with self.settings(
|
||||||
EMAIL_BACKEND="django.core.mail.backends.locmem.EmailBackend"
|
EMAIL_BACKEND="django.core.mail.backends.locmem.EmailBackend"
|
||||||
|
@ -67,7 +67,7 @@ class TestEmailStageSending(TestCase):
|
||||||
session.save()
|
session.save()
|
||||||
|
|
||||||
url = reverse(
|
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(
|
with self.settings(
|
||||||
EMAIL_BACKEND="django.core.mail.backends.locmem.EmailBackend"
|
EMAIL_BACKEND="django.core.mail.backends.locmem.EmailBackend"
|
||||||
|
|
|
@ -46,7 +46,7 @@ class TestEmailStage(TestCase):
|
||||||
session.save()
|
session.save()
|
||||||
|
|
||||||
url = reverse(
|
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)
|
response = self.client.get(url)
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
@ -61,7 +61,7 @@ class TestEmailStage(TestCase):
|
||||||
session.save()
|
session.save()
|
||||||
|
|
||||||
url = reverse(
|
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)
|
response = self.client.get(url)
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
@ -77,7 +77,7 @@ class TestEmailStage(TestCase):
|
||||||
session.save()
|
session.save()
|
||||||
|
|
||||||
url = reverse(
|
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(
|
with self.settings(
|
||||||
EMAIL_BACKEND="django.core.mail.backends.locmem.EmailBackend"
|
EMAIL_BACKEND="django.core.mail.backends.locmem.EmailBackend"
|
||||||
|
@ -118,7 +118,7 @@ class TestEmailStage(TestCase):
|
||||||
# Call the actual executor to get the JSON Response
|
# Call the actual executor to get the JSON Response
|
||||||
response = self.client.get(
|
response = self.client.get(
|
||||||
reverse(
|
reverse(
|
||||||
"authentik_flows:flow-executor",
|
"authentik_api:flow-executor",
|
||||||
kwargs={"flow_slug": self.flow.slug},
|
kwargs={"flow_slug": self.flow.slug},
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
|
@ -7,64 +7,33 @@ from django.http import HttpResponse
|
||||||
from django.shortcuts import reverse
|
from django.shortcuts import reverse
|
||||||
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 rest_framework.fields import CharField
|
||||||
from structlog.stdlib import get_logger
|
from structlog.stdlib import get_logger
|
||||||
|
|
||||||
from authentik.core.models import Source, User
|
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.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.flows.views import SESSION_KEY_APPLICATION_PRE
|
||||||
from authentik.stages.identification.forms import IdentificationForm
|
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()
|
LOGGER = get_logger()
|
||||||
|
|
||||||
|
|
||||||
class IdentificationStageView(FormView, StageView):
|
class IdentificationChallengeResponse(ChallengeResponse):
|
||||||
|
|
||||||
|
uid_field = CharField()
|
||||||
|
|
||||||
|
|
||||||
|
class IdentificationStageView(ChallengeStageView):
|
||||||
"""Form to identify the user"""
|
"""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]:
|
def get_user(self, uid_value: str) -> Optional[User]:
|
||||||
"""Find user instance. Returns None if no user was found."""
|
"""Find user instance. Returns None if no user was found."""
|
||||||
current_stage: IdentificationStage = self.executor.current_stage
|
current_stage: IdentificationStage = self.executor.current_stage
|
||||||
|
@ -82,14 +51,54 @@ class IdentificationStageView(FormView, StageView):
|
||||||
return users.first()
|
return users.first()
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def form_valid(self, form: IdentificationForm) -> HttpResponse:
|
def get_challenge(self) -> Challenge:
|
||||||
"""Form data is valid"""
|
current_stage: IdentificationStage = self.executor.current_stage
|
||||||
user_identifier = form.cleaned_data.get("uid_field")
|
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)
|
pre_user = self.get_user(user_identifier)
|
||||||
if not pre_user:
|
if not pre_user:
|
||||||
LOGGER.debug("invalid_login")
|
LOGGER.debug("invalid_login")
|
||||||
messages.error(self.request, _("Failed to authenticate."))
|
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
|
self.executor.plan.context[PLAN_CONTEXT_PENDING_USER] = pre_user
|
||||||
|
|
||||||
current_stage: IdentificationStage = self.executor.current_stage
|
current_stage: IdentificationStage = self.executor.current_stage
|
||||||
|
|
|
@ -43,9 +43,7 @@ class TestIdentificationStage(TestCase):
|
||||||
def test_valid_render(self):
|
def test_valid_render(self):
|
||||||
"""Test that View renders correctly"""
|
"""Test that View renders correctly"""
|
||||||
response = self.client.get(
|
response = self.client.get(
|
||||||
reverse(
|
reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug})
|
||||||
"authentik_flows:flow-executor", kwargs={"flow_slug": self.flow.slug}
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
@ -53,7 +51,7 @@ class TestIdentificationStage(TestCase):
|
||||||
"""Test with valid email, check that URL redirects back to itself"""
|
"""Test with valid email, check that URL redirects back to itself"""
|
||||||
form_data = {"uid_field": self.user.email}
|
form_data = {"uid_field": self.user.email}
|
||||||
url = reverse(
|
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)
|
response = self.client.post(url, form_data)
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
@ -67,7 +65,7 @@ class TestIdentificationStage(TestCase):
|
||||||
form_data = {"uid_field": self.user.username}
|
form_data = {"uid_field": self.user.username}
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
reverse(
|
reverse(
|
||||||
"authentik_flows:flow-executor", kwargs={"flow_slug": self.flow.slug}
|
"authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}
|
||||||
),
|
),
|
||||||
form_data,
|
form_data,
|
||||||
)
|
)
|
||||||
|
@ -78,7 +76,7 @@ class TestIdentificationStage(TestCase):
|
||||||
form_data = {"uid_field": self.user.email + "test"}
|
form_data = {"uid_field": self.user.email + "test"}
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
reverse(
|
reverse(
|
||||||
"authentik_flows:flow-executor", kwargs={"flow_slug": self.flow.slug}
|
"authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}
|
||||||
),
|
),
|
||||||
form_data,
|
form_data,
|
||||||
)
|
)
|
||||||
|
@ -101,7 +99,7 @@ class TestIdentificationStage(TestCase):
|
||||||
|
|
||||||
response = self.client.get(
|
response = self.client.get(
|
||||||
reverse(
|
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)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
@ -124,7 +122,7 @@ class TestIdentificationStage(TestCase):
|
||||||
|
|
||||||
response = self.client.get(
|
response = self.client.get(
|
||||||
reverse(
|
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)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
|
@ -58,9 +58,7 @@ class TestUserLoginStage(TestCase):
|
||||||
session.save()
|
session.save()
|
||||||
|
|
||||||
response = self.client.get(
|
response = self.client.get(
|
||||||
reverse(
|
reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug})
|
||||||
"authentik_flows:flow-executor", kwargs={"flow_slug": self.flow.slug}
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
self.assertIsInstance(response, AccessDeniedResponse)
|
self.assertIsInstance(response, AccessDeniedResponse)
|
||||||
|
@ -81,9 +79,7 @@ class TestUserLoginStage(TestCase):
|
||||||
session.save()
|
session.save()
|
||||||
|
|
||||||
response = self.client.get(
|
response = self.client.get(
|
||||||
reverse(
|
reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug})
|
||||||
"authentik_flows:flow-executor", kwargs={"flow_slug": self.flow.slug}
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
@ -115,7 +111,7 @@ class TestUserLoginStage(TestCase):
|
||||||
|
|
||||||
with patch("authentik.flows.views.FlowExecutorView.cancel", MagicMock()):
|
with patch("authentik.flows.views.FlowExecutorView.cancel", MagicMock()):
|
||||||
base_url = reverse(
|
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(
|
response = self.client.get(
|
||||||
base_url + f"?{INVITATION_TOKEN_KEY}={invite.pk.hex}"
|
base_url + f"?{INVITATION_TOKEN_KEY}={invite.pk.hex}"
|
||||||
|
|
|
@ -5,6 +5,6 @@
|
||||||
|
|
||||||
{% block beneath_form %}
|
{% block beneath_form %}
|
||||||
{% if recovery_flow %}
|
{% if recovery_flow %}
|
||||||
<a href="{% url 'authentik_flows:flow-executor' flow_slug=recovery_flow.slug %}">{% trans 'Forgot password?' %}</a>
|
<a href="{% url 'authentik_api:flow-executor' flow_slug=recovery_flow.slug %}">{% trans 'Forgot password?' %}</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -59,7 +59,7 @@ class TestPasswordStage(TestCase):
|
||||||
|
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
reverse(
|
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
|
# Still have to send the password so the form is valid
|
||||||
{"password": self.password},
|
{"password": self.password},
|
||||||
|
@ -83,7 +83,7 @@ class TestPasswordStage(TestCase):
|
||||||
|
|
||||||
response = self.client.get(
|
response = self.client.get(
|
||||||
reverse(
|
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)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
@ -101,7 +101,7 @@ class TestPasswordStage(TestCase):
|
||||||
|
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
reverse(
|
reverse(
|
||||||
"authentik_flows:flow-executor", kwargs={"flow_slug": self.flow.slug}
|
"authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}
|
||||||
),
|
),
|
||||||
# Form data
|
# Form data
|
||||||
{"password": self.password},
|
{"password": self.password},
|
||||||
|
@ -125,7 +125,7 @@ class TestPasswordStage(TestCase):
|
||||||
|
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
reverse(
|
reverse(
|
||||||
"authentik_flows:flow-executor", kwargs={"flow_slug": self.flow.slug}
|
"authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}
|
||||||
),
|
),
|
||||||
# Form data
|
# Form data
|
||||||
{"password": self.password + "test"},
|
{"password": self.password + "test"},
|
||||||
|
@ -145,7 +145,7 @@ class TestPasswordStage(TestCase):
|
||||||
for _ in range(self.stage.failed_attempts_before_cancel):
|
for _ in range(self.stage.failed_attempts_before_cancel):
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
reverse(
|
reverse(
|
||||||
"authentik_flows:flow-executor",
|
"authentik_api:flow-executor",
|
||||||
kwargs={"flow_slug": self.flow.slug},
|
kwargs={"flow_slug": self.flow.slug},
|
||||||
),
|
),
|
||||||
# Form data
|
# Form data
|
||||||
|
@ -155,7 +155,7 @@ class TestPasswordStage(TestCase):
|
||||||
|
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
reverse(
|
reverse(
|
||||||
"authentik_flows:flow-executor", kwargs={"flow_slug": self.flow.slug}
|
"authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}
|
||||||
),
|
),
|
||||||
# Form data
|
# Form data
|
||||||
{"password": self.password + "test"},
|
{"password": self.password + "test"},
|
||||||
|
@ -185,7 +185,7 @@ class TestPasswordStage(TestCase):
|
||||||
|
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
reverse(
|
reverse(
|
||||||
"authentik_flows:flow-executor", kwargs={"flow_slug": self.flow.slug}
|
"authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}
|
||||||
),
|
),
|
||||||
# Form data
|
# Form data
|
||||||
{"password": self.password + "test"},
|
{"password": self.password + "test"},
|
||||||
|
|
|
@ -104,9 +104,7 @@ class TestPromptStage(TestCase):
|
||||||
session.save()
|
session.save()
|
||||||
|
|
||||||
response = self.client.get(
|
response = self.client.get(
|
||||||
reverse(
|
reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug})
|
||||||
"authentik_flows:flow-executor", kwargs={"flow_slug": self.flow.slug}
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
for prompt in self.stage.fields.all():
|
for prompt in self.stage.fields.all():
|
||||||
|
@ -158,7 +156,7 @@ class TestPromptStage(TestCase):
|
||||||
with patch("authentik.flows.views.FlowExecutorView.cancel", MagicMock()):
|
with patch("authentik.flows.views.FlowExecutorView.cancel", MagicMock()):
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
reverse(
|
reverse(
|
||||||
"authentik_flows:flow-executor",
|
"authentik_api:flow-executor",
|
||||||
kwargs={"flow_slug": self.flow.slug},
|
kwargs={"flow_slug": self.flow.slug},
|
||||||
),
|
),
|
||||||
form.cleaned_data,
|
form.cleaned_data,
|
||||||
|
|
|
@ -46,9 +46,7 @@ class TestUserDeleteStage(TestCase):
|
||||||
session.save()
|
session.save()
|
||||||
|
|
||||||
response = self.client.get(
|
response = self.client.get(
|
||||||
reverse(
|
reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug})
|
||||||
"authentik_flows:flow-executor", kwargs={"flow_slug": self.flow.slug}
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
self.assertIsInstance(response, AccessDeniedResponse)
|
self.assertIsInstance(response, AccessDeniedResponse)
|
||||||
|
@ -64,9 +62,7 @@ class TestUserDeleteStage(TestCase):
|
||||||
session.save()
|
session.save()
|
||||||
|
|
||||||
response = self.client.get(
|
response = self.client.get(
|
||||||
reverse(
|
reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug})
|
||||||
"authentik_flows:flow-executor", kwargs={"flow_slug": self.flow.slug}
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
@ -82,7 +78,7 @@ class TestUserDeleteStage(TestCase):
|
||||||
|
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
reverse(
|
reverse(
|
||||||
"authentik_flows:flow-executor", kwargs={"flow_slug": self.flow.slug}
|
"authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}
|
||||||
),
|
),
|
||||||
{},
|
{},
|
||||||
)
|
)
|
||||||
|
|
|
@ -47,9 +47,7 @@ class TestUserLoginStage(TestCase):
|
||||||
session.save()
|
session.save()
|
||||||
|
|
||||||
response = self.client.get(
|
response = self.client.get(
|
||||||
reverse(
|
reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug})
|
||||||
"authentik_flows:flow-executor", kwargs={"flow_slug": self.flow.slug}
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
@ -72,9 +70,7 @@ class TestUserLoginStage(TestCase):
|
||||||
session.save()
|
session.save()
|
||||||
|
|
||||||
response = self.client.get(
|
response = self.client.get(
|
||||||
reverse(
|
reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug})
|
||||||
"authentik_flows:flow-executor", kwargs={"flow_slug": self.flow.slug}
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
@ -95,9 +91,7 @@ class TestUserLoginStage(TestCase):
|
||||||
session.save()
|
session.save()
|
||||||
|
|
||||||
response = self.client.get(
|
response = self.client.get(
|
||||||
reverse(
|
reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug})
|
||||||
"authentik_flows:flow-executor", kwargs={"flow_slug": self.flow.slug}
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
|
@ -43,9 +43,7 @@ class TestUserLogoutStage(TestCase):
|
||||||
session.save()
|
session.save()
|
||||||
|
|
||||||
response = self.client.get(
|
response = self.client.get(
|
||||||
reverse(
|
reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug})
|
||||||
"authentik_flows:flow-executor", kwargs={"flow_slug": self.flow.slug}
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
|
@ -55,9 +55,7 @@ class TestUserWriteStage(TestCase):
|
||||||
session.save()
|
session.save()
|
||||||
|
|
||||||
response = self.client.get(
|
response = self.client.get(
|
||||||
reverse(
|
reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug})
|
||||||
"authentik_flows:flow-executor", kwargs={"flow_slug": self.flow.slug}
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
@ -94,9 +92,7 @@ class TestUserWriteStage(TestCase):
|
||||||
session.save()
|
session.save()
|
||||||
|
|
||||||
response = self.client.get(
|
response = self.client.get(
|
||||||
reverse(
|
reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug})
|
||||||
"authentik_flows:flow-executor", kwargs={"flow_slug": self.flow.slug}
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
@ -126,9 +122,7 @@ class TestUserWriteStage(TestCase):
|
||||||
session.save()
|
session.save()
|
||||||
|
|
||||||
response = self.client.get(
|
response = self.client.get(
|
||||||
reverse(
|
reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug})
|
||||||
"authentik_flows:flow-executor", kwargs={"flow_slug": self.flow.slug}
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
|
@ -48,4 +48,33 @@ export default [
|
||||||
},
|
},
|
||||||
external: ["django"]
|
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"]
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
|
@ -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`;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
10
web/src/elements/stages/base.ts
Normal file
10
web/src/elements/stages/base.ts
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
import { LitElement } from "lit-element";
|
||||||
|
import { FlowExecutor } from "../../pages/generic/FlowExecutor";
|
||||||
|
|
||||||
|
export class BaseStage extends LitElement {
|
||||||
|
|
||||||
|
// submit()
|
||||||
|
|
||||||
|
host?: FlowExecutor;
|
||||||
|
|
||||||
|
}
|
101
web/src/elements/stages/identification/IdentificationStage.ts
Normal file
101
web/src/elements/stages/identification/IdentificationStage.ts
Normal file
|
@ -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`<ak-loading-state></ak-loading-state>`;
|
||||||
|
}
|
||||||
|
return html`<header class="pf-c-login__main-header">
|
||||||
|
<h1 class="pf-c-title pf-m-3xl">
|
||||||
|
${gettext("Log in to your account")}
|
||||||
|
</h1>
|
||||||
|
</header>
|
||||||
|
<div class="pf-c-login__main-body">
|
||||||
|
<form class="pf-c-form">
|
||||||
|
${this.args.application_pre ?
|
||||||
|
html`<p>
|
||||||
|
${gettext(`Login to continue to ${this.args.application_pre}.`)}
|
||||||
|
</p>`:
|
||||||
|
html``}
|
||||||
|
|
||||||
|
<div class="pf-c-form__group">
|
||||||
|
<label class="pf-c-form__label" for="uid_field-0">
|
||||||
|
<span class="pf-c-form__label-text">${gettext("Email or Username")}</span>
|
||||||
|
<span class="pf-c-form__label-required" aria-hidden="true">*</span>
|
||||||
|
</label>
|
||||||
|
<input type="text" name="uid_field" placeholder="Email or Username" autofocus autocomplete="username" class="pf-c-form-control" required="" id="id_uid_field">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pf-c-form__group pf-m-action">
|
||||||
|
<button class="pf-c-button pf-m-primary pf-m-block" @click=${(e: Event) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const form = new FormData(this.shadowRoot.querySelector("form"));
|
||||||
|
this.host?.submit(form);
|
||||||
|
}}>
|
||||||
|
${this.args.primary_action}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<footer class="pf-c-login__main-footer">
|
||||||
|
<ul class="pf-c-login__main-footer-links">
|
||||||
|
${this.args.sources.map(() => {
|
||||||
|
// TODO: source testing
|
||||||
|
// TODO: Placeholder and label for input above
|
||||||
|
return html``;
|
||||||
|
// {% for source in sources %}
|
||||||
|
// <li class="pf-c-login__main-footer-links-item">
|
||||||
|
// <a href="{{ source.url }}" class="pf-c-login__main-footer-links-item-link">
|
||||||
|
// {% if source.icon_path %}
|
||||||
|
// <img src="{% static source.icon_path %}" style="width:24px;" alt="{{ source.name }}">
|
||||||
|
// {% elif source.icon_url %}
|
||||||
|
// <img src="icon_url" alt="{{ source.name }}">
|
||||||
|
// {% else %}
|
||||||
|
// <i class="pf-icon pf-icon-arrow" title="{{ source.name }}"></i>
|
||||||
|
// {% endif %}
|
||||||
|
// </a>
|
||||||
|
// </li>
|
||||||
|
// {% endfor %}
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
{% if enroll_url or recovery_url %}
|
||||||
|
<div class="pf-c-login__main-footer-band">
|
||||||
|
{% if enroll_url %}
|
||||||
|
<p class="pf-c-login__main-footer-band-item">
|
||||||
|
{% trans 'Need an account?' %}
|
||||||
|
<a role="enroll" href="{{ enroll_url }}">{% trans 'Sign up.' %}</a>
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
|
{% if recovery_url %}
|
||||||
|
<p class="pf-c-login__main-footer-band-item">
|
||||||
|
<a role="recovery" href="{{ recovery_url }}">{% trans 'Forgot username or password?' %}</a>
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</footer>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
3
web/src/flow.ts
Normal file
3
web/src/flow.ts
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
import "construct-style-sheets-polyfill";
|
||||||
|
|
||||||
|
import "./pages/generic/FlowExecutor";
|
|
@ -22,7 +22,6 @@ import "./elements/Spinner";
|
||||||
import "./elements/Tabs";
|
import "./elements/Tabs";
|
||||||
import "./elements/router/RouterOutlet";
|
import "./elements/router/RouterOutlet";
|
||||||
|
|
||||||
import "./pages/generic/FlowShellCard";
|
|
||||||
import "./pages/generic/SiteShell";
|
import "./pages/generic/SiteShell";
|
||||||
|
|
||||||
import "./pages/admin-overview/AdminOverviewPage";
|
import "./pages/admin-overview/AdminOverviewPage";
|
||||||
|
@ -33,5 +32,7 @@ import "./pages/LibraryPage";
|
||||||
|
|
||||||
import "./elements/stages/authenticator_webauthn/WebAuthnRegister";
|
import "./elements/stages/authenticator_webauthn/WebAuthnRegister";
|
||||||
import "./elements/stages/authenticator_webauthn/WebAuthnAuth";
|
import "./elements/stages/authenticator_webauthn/WebAuthnAuth";
|
||||||
|
import "./elements/stages/authenticator_validate/AuthenticatorValidateStage";
|
||||||
|
import "./elements/stages/identification/IdentificationStage";
|
||||||
|
|
||||||
import "./interfaces/AdminInterface";
|
import "./interfaces/AdminInterface";
|
||||||
|
|
|
@ -7,6 +7,7 @@ import "../../elements/Tabs";
|
||||||
import "../../elements/AdminLoginsChart";
|
import "../../elements/AdminLoginsChart";
|
||||||
import "../../elements/buttons/ModalButton";
|
import "../../elements/buttons/ModalButton";
|
||||||
import "../../elements/buttons/SpinnerButton";
|
import "../../elements/buttons/SpinnerButton";
|
||||||
|
import "../../elements/buttons/Dropdown";
|
||||||
import "../../elements/policies/BoundPoliciesList";
|
import "../../elements/policies/BoundPoliciesList";
|
||||||
import { FlowStageBinding, Stage } from "../../api/Flows";
|
import { FlowStageBinding, Stage } from "../../api/Flows";
|
||||||
import { until } from "lit-html/directives/until";
|
import { until } from "lit-html/directives/until";
|
||||||
|
|
|
@ -1,25 +1,30 @@
|
||||||
|
import { gettext } from "django";
|
||||||
import { LitElement, html, customElement, property, TemplateResult } from "lit-element";
|
import { LitElement, html, customElement, property, TemplateResult } from "lit-element";
|
||||||
|
import { unsafeHTML } from "lit-html/directives/unsafe-html";
|
||||||
import { SentryIgnoredError } from "../../common/errors";
|
import { SentryIgnoredError } from "../../common/errors";
|
||||||
import { getCookie } from "../../utils";
|
import { getCookie } from "../../utils";
|
||||||
|
import "../../elements/stages/identification/IdentificationStage";
|
||||||
|
|
||||||
enum ResponseType {
|
enum ChallengeTypes {
|
||||||
|
native = "native",
|
||||||
|
response = "response",
|
||||||
|
shell = "shell",
|
||||||
redirect = "redirect",
|
redirect = "redirect",
|
||||||
template = "template",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Response {
|
interface Challenge {
|
||||||
type: ResponseType;
|
type: ChallengeTypes;
|
||||||
to?: string;
|
args: { [key: string]: string };
|
||||||
body?: string;
|
component: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@customElement("ak-flow-shell-card")
|
@customElement("ak-flow-executor")
|
||||||
export class FlowShellCard extends LitElement {
|
export class FlowExecutor extends LitElement {
|
||||||
@property()
|
@property()
|
||||||
flowBodyUrl = "";
|
flowBodyUrl = "";
|
||||||
|
|
||||||
@property()
|
@property({attribute: false})
|
||||||
flowBody?: string;
|
flowBody?: TemplateResult;
|
||||||
|
|
||||||
createRenderRoot(): Element | ShadowRoot {
|
createRenderRoot(): Element | ShadowRoot {
|
||||||
return this;
|
return this;
|
||||||
|
@ -28,6 +33,11 @@ export class FlowShellCard extends LitElement {
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this.addEventListener("ak-flow-submit", () => {
|
this.addEventListener("ak-flow-submit", () => {
|
||||||
|
this.submit();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
submit(formData?: FormData): void {
|
||||||
const csrftoken = getCookie("authentik_csrf");
|
const csrftoken = getCookie("authentik_csrf");
|
||||||
const request = new Request(this.flowBodyUrl, {
|
const request = new Request(this.flowBodyUrl, {
|
||||||
headers: {
|
headers: {
|
||||||
|
@ -36,7 +46,8 @@ export class FlowShellCard extends LitElement {
|
||||||
});
|
});
|
||||||
fetch(request, {
|
fetch(request, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
mode: "same-origin"
|
mode: "same-origin",
|
||||||
|
body: formData,
|
||||||
})
|
})
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
return response.json();
|
return response.json();
|
||||||
|
@ -47,7 +58,6 @@ export class FlowShellCard extends LitElement {
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
this.errorMessage(e);
|
this.errorMessage(e);
|
||||||
});
|
});
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
firstUpdated(): void {
|
firstUpdated(): void {
|
||||||
|
@ -73,19 +83,29 @@ export class FlowShellCard extends LitElement {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateCard(data: Response): Promise<void> {
|
async updateCard(data: Challenge): Promise<void> {
|
||||||
switch (data.type) {
|
switch (data.type) {
|
||||||
case ResponseType.redirect:
|
case ChallengeTypes.redirect:
|
||||||
console.debug(`authentik/flows: redirecting to ${data.to}`);
|
console.debug(`authentik/flows: redirecting to ${data.args.to}`);
|
||||||
window.location.assign(data.to || "");
|
window.location.assign(data.args.to || "");
|
||||||
break;
|
break;
|
||||||
case ResponseType.template:
|
case ChallengeTypes.shell:
|
||||||
this.flowBody = data.body;
|
this.flowBody = html`${unsafeHTML(data.args.body)}`;
|
||||||
await this.requestUpdate();
|
await this.requestUpdate();
|
||||||
this.checkAutofocus();
|
this.checkAutofocus();
|
||||||
this.loadFormCode();
|
this.loadFormCode();
|
||||||
this.setFormSubmitHandlers();
|
this.setFormSubmitHandlers();
|
||||||
break;
|
break;
|
||||||
|
case ChallengeTypes.native:
|
||||||
|
switch (data.component) {
|
||||||
|
case "ak-stage-identification":
|
||||||
|
this.flowBody = html`<ak-stage-identification .host=${this} .args=${data.args}></ak-stage-identification>`;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// this.flowBody = html`${unsafeHTML(`<${data.component} .args="${data.args}"></${data.component}>`)}`;
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
console.debug(`authentik/flows: unexpected data type ${data.type}`);
|
console.debug(`authentik/flows: unexpected data type ${data.type}`);
|
||||||
break;
|
break;
|
||||||
|
@ -139,26 +159,14 @@ export class FlowShellCard extends LitElement {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const formData = new FormData(form);
|
const formData = new FormData(form);
|
||||||
this.flowBody = undefined;
|
this.flowBody = undefined;
|
||||||
fetch(this.flowBodyUrl, {
|
this.submit(formData);
|
||||||
method: "post",
|
|
||||||
body: formData,
|
|
||||||
})
|
|
||||||
.then((response) => {
|
|
||||||
return response.json();
|
|
||||||
})
|
|
||||||
.then((data) => {
|
|
||||||
this.updateCard(data);
|
|
||||||
})
|
|
||||||
.catch((e) => {
|
|
||||||
this.errorMessage(e);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
form.classList.add("ak-flow-wrapped");
|
form.classList.add("ak-flow-wrapped");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
errorMessage(error: string): void {
|
errorMessage(error: string): void {
|
||||||
this.flowBody = `
|
this.flowBody = html`
|
||||||
<style>
|
<style>
|
||||||
.ak-exception {
|
.ak-exception {
|
||||||
font-family: monospace;
|
font-family: monospace;
|
||||||
|
@ -167,13 +175,11 @@ export class FlowShellCard extends LitElement {
|
||||||
</style>
|
</style>
|
||||||
<header class="pf-c-login__main-header">
|
<header class="pf-c-login__main-header">
|
||||||
<h1 class="pf-c-title pf-m-3xl">
|
<h1 class="pf-c-title pf-m-3xl">
|
||||||
Whoops!
|
${gettext("Whoops!")}
|
||||||
</h1>
|
</h1>
|
||||||
</header>
|
</header>
|
||||||
<div class="pf-c-login__main-body">
|
<div class="pf-c-login__main-body">
|
||||||
<h3>
|
<h3>${gettext("Something went wrong! Please try again later.")}</h3>
|
||||||
Something went wrong! Please try again later.
|
|
||||||
</h3>
|
|
||||||
<pre class="ak-exception">${error}</pre>
|
<pre class="ak-exception">${error}</pre>
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
|
@ -190,7 +196,7 @@ export class FlowShellCard extends LitElement {
|
||||||
|
|
||||||
render(): TemplateResult {
|
render(): TemplateResult {
|
||||||
if (this.flowBody) {
|
if (this.flowBody) {
|
||||||
return html(<TemplateStringsArray>(<unknown>[this.flowBody]));
|
return this.flowBody;
|
||||||
}
|
}
|
||||||
return this.loading();
|
return this.loading();
|
||||||
}
|
}
|
Reference in a new issue