flows: keep ?next url when using cancel (#18619)

keep ?next url when using cancel

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
This commit is contained in:
Jens L.
2025-12-05 15:35:15 +01:00
committed by GitHub
parent 1244a40ffb
commit 024e6c1961
4 changed files with 66 additions and 3 deletions
+12 -1
View File
@@ -1,6 +1,7 @@
"""authentik stage Base view"""
from typing import TYPE_CHECKING
from urllib.parse import urlencode
from django.conf import settings
from django.contrib.auth.models import AnonymousUser
@@ -164,6 +165,16 @@ class ChallengeStageView(StageView):
self.logger.warning("failed to template title", exc=exc)
return self.executor.flow.title
@property
def cancel_url(self) -> str:
from authentik.flows.views.executor import NEXT_ARG_NAME, SESSION_KEY_GET
next_param = self.request.session.get(SESSION_KEY_GET, {}).get(NEXT_ARG_NAME)
url = reverse("authentik_flows:cancel")
if next_param:
return f"{url}?{urlencode({NEXT_ARG_NAME: next_param})}"
return url
def _get_challenge(self, *args, **kwargs) -> Challenge:
with (
start_span(
@@ -186,7 +197,7 @@ class ChallengeStageView(StageView):
data={
"title": self.format_title(),
"background": self.executor.flow.background_url(self.request),
"cancel_url": reverse("authentik_flows:cancel"),
"cancel_url": self.cancel_url,
"layout": self.executor.flow.layout,
}
)
+4 -2
View File
@@ -32,8 +32,10 @@ class FlowTestCase(APITestCase):
self.assertIsNotNone(raw_response["component"])
if flow:
self.assertIn("flow_info", raw_response)
self.assertEqual(
raw_response["flow_info"]["cancel_url"], reverse("authentik_flows:cancel")
self.assertTrue(
raw_response["flow_info"]["cancel_url"].startswith(
reverse("authentik_flows:cancel")
)
)
# We don't check the flow title since it will most likely go
# through ChallengeStageView.format_title() so might not match 1:1
+47
View File
@@ -36,6 +36,7 @@ from authentik.policies.types import PolicyResult
from authentik.stages.deny.models import DenyStage
from authentik.stages.dummy.models import DummyStage
from authentik.stages.identification.models import IdentificationStage, UserFields
from authentik.stages.password.models import PasswordStage
POLICY_RETURN_FALSE = PropertyMock(return_value=PolicyResult(False, "foo"))
POLICY_RETURN_TRUE = MagicMock(return_value=PolicyResult(True))
@@ -692,3 +693,49 @@ class TestFlowExecutor(FlowTestCase):
self.client.logout()
response = self.client.post(url, data="{", content_type="application/json")
self.assertEqual(response.status_code, 200)
def test_cancel_next(self):
"""Test cancel URL with ?next param set"""
flow = create_test_flow()
# Stage 0 is an identification stage
ident_stage = IdentificationStage.objects.create(
name=generate_id(),
user_fields=[UserFields.USERNAME],
)
FlowStageBinding.objects.create(
target=flow,
stage=ident_stage,
order=0,
)
# Stage 1 is a password stage
password_stage = PasswordStage.objects.create(name=generate_id(), backends=[])
FlowStageBinding.objects.create(
target=flow,
stage=password_stage,
order=1,
)
res = self.client.get(
reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug})
+ f"?{urlencode({QS_QUERY: urlencode({NEXT_ARG_NAME: "/foo"})})}"
)
self.assertStageResponse(res, flow, component="ak-stage-identification")
res = self.client.post(
reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug})
+ f"?{urlencode({QS_QUERY: urlencode({NEXT_ARG_NAME: "/foo"})})}",
data={"component": "ak-stage-identification", "uid_field": generate_id()},
follow=True,
)
self.assertEqual(res.status_code, 200)
self.assertStageResponse(
res,
flow,
flow_info={
"background": "/static/dist/assets/images/flow_background.jpg",
"cancel_url": "/flows/-/cancel/?next=%2Ffoo",
"layout": "stacked",
"title": flow.title,
},
)
+3
View File
@@ -479,6 +479,9 @@ class CancelView(View):
if SESSION_KEY_PLAN in request.session:
del request.session[SESSION_KEY_PLAN]
LOGGER.debug("Canceled current plan")
next_url = self.request.GET.get(NEXT_ARG_NAME)
if next_url and not is_url_absolute(next_url):
return redirect(next_url)
return redirect("authentik_flows:default-invalidation")