flows: mount executor under api, implement initial challenge design

This commit is contained in:
Jens Langhammer 2021-02-17 23:52:49 +01:00
parent 8708e487ae
commit eb01b42425
33 changed files with 482 additions and 218 deletions

View file

@ -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

View file

@ -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>

View file

@ -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 %}

View 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)

View file

@ -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)

View file

@ -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 %}

View file

@ -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()

View file

@ -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)

View file

@ -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

View file

@ -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()

View file

@ -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"},
) )

View file

@ -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)

View file

@ -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)

View file

@ -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"

View file

@ -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},
) )
) )

View file

@ -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

View file

@ -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)

View file

@ -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}"

View file

@ -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 %}

View file

@ -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"},

View file

@ -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,

View file

@ -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}
), ),
{}, {},
) )

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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"]
},
]; ];

View file

@ -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`;
}
}

View file

@ -0,0 +1,10 @@
import { LitElement } from "lit-element";
import { FlowExecutor } from "../../pages/generic/FlowExecutor";
export class BaseStage extends LitElement {
// submit()
host?: FlowExecutor;
}

View 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
View file

@ -0,0 +1,3 @@
import "construct-style-sheets-polyfill";
import "./pages/generic/FlowExecutor";

View file

@ -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";

View file

@ -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";

View file

@ -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,28 +33,33 @@ export class FlowShellCard extends LitElement {
constructor() { constructor() {
super(); super();
this.addEventListener("ak-flow-submit", () => { this.addEventListener("ak-flow-submit", () => {
const csrftoken = getCookie("authentik_csrf"); this.submit();
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);
});
}); });
} }
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 { firstUpdated(): void {
fetch(this.flowBodyUrl) fetch(this.flowBodyUrl)
.then((r) => { .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) { 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();
} }