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,
|
||||
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/<slug:flow_slug>/",
|
||||
FlowExecutorView.as_view(),
|
||||
name="flow-executor",
|
||||
),
|
||||
] + 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/authentik.css' %}?v={{ ak_version }}">
|
||||
<script src="{% url 'javascript-catalog' %}?v={{ ak_version }}"></script>
|
||||
<script src="{% static 'dist/main.js' %}?v={{ ak_version }}" type="module"></script>
|
||||
{% block head %}
|
||||
{% endblock %}
|
||||
</head>
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
{% extends "base/skeleton.html" %}
|
||||
|
||||
{% block head %}
|
||||
<script src="{% static 'dist/main.js' %}?v={{ ak_version }}" type="module"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<ak-interface-admin></ak-interface-admin>
|
||||
{% endblock %}
|
||||
|
|
|
@ -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 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)
|
||||
|
|
|
@ -22,11 +22,10 @@
|
|||
background-position: center;
|
||||
}
|
||||
</style>
|
||||
<script src="{% static 'dist/flow.js' %}?v={{ ak_version }}" type="module"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block main_container %}
|
||||
<ak-flow-shell-card
|
||||
class="pf-c-login__main"
|
||||
flowBodyUrl="{{ exec_url }}">
|
||||
</ak-flow-shell-card>
|
||||
<ak-flow-executor class="pf-c-login__main" flowBodyUrl="{{ exec_url }}">
|
||||
</ak-flow-executor>
|
||||
{% endblock %}
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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"},
|
||||
)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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},
|
||||
)
|
||||
)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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}"
|
||||
|
|
|
@ -5,6 +5,6 @@
|
|||
|
||||
{% block beneath_form %}
|
||||
{% 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 %}
|
||||
{% endblock %}
|
||||
|
|
|
@ -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"},
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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}
|
||||
),
|
||||
{},
|
||||
)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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"]
|
||||
},
|
||||
];
|
||||
|
|
|
@ -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`;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
import { LitElement } from "lit-element";
|
||||
import { FlowExecutor } from "../../pages/generic/FlowExecutor";
|
||||
|
||||
export class BaseStage extends LitElement {
|
||||
|
||||
// submit()
|
||||
|
||||
host?: FlowExecutor;
|
||||
|
||||
}
|
|
@ -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>`;
|
||||
}
|
||||
|
||||
}
|
|
@ -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/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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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<void> {
|
||||
async updateCard(data: Challenge): Promise<void> {
|
||||
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`<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:
|
||||
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`
|
||||
<style>
|
||||
.ak-exception {
|
||||
font-family: monospace;
|
||||
|
@ -167,13 +175,11 @@ export class FlowShellCard extends LitElement {
|
|||
</style>
|
||||
<header class="pf-c-login__main-header">
|
||||
<h1 class="pf-c-title pf-m-3xl">
|
||||
Whoops!
|
||||
${gettext("Whoops!")}
|
||||
</h1>
|
||||
</header>
|
||||
<div class="pf-c-login__main-body">
|
||||
<h3>
|
||||
Something went wrong! Please try again later.
|
||||
</h3>
|
||||
<h3>${gettext("Something went wrong! Please try again later.")}</h3>
|
||||
<pre class="ak-exception">${error}</pre>
|
||||
</div>`;
|
||||
}
|
||||
|
@ -190,7 +196,7 @@ export class FlowShellCard extends LitElement {
|
|||
|
||||
render(): TemplateResult {
|
||||
if (this.flowBody) {
|
||||
return html(<TemplateStringsArray>(<unknown>[this.flowBody]));
|
||||
return this.flowBody;
|
||||
}
|
||||
return this.loading();
|
||||
}
|
Reference in New Issue