diff --git a/authentik/brands/tests.py b/authentik/brands/tests.py
index 41fcd98040..337c304794 100644
--- a/authentik/brands/tests.py
+++ b/authentik/brands/tests.py
@@ -20,11 +20,16 @@ class TestBrands(APITestCase):
def setUp(self):
super().setUp()
- self.default_flags = {}
- for flag in Flag.available(visibility="public"):
- self.default_flags[flag().key] = flag.get()
Brand.objects.all().delete()
+ @property
+ def default_flags(self) -> dict[str, object]:
+ """Get current public flags.
+
+ Some tests define temporary Flag subclasses, so this can't be cached in setUp.
+ """
+ return {flag().key: flag.get() for flag in Flag.available(visibility="public")}
+
def test_current_brand(self):
"""Test Current brand API"""
brand = create_test_brand()
diff --git a/authentik/core/templates/login/base_full.html b/authentik/core/templates/login/base_full.html
index 1e55116354..f3554446e8 100644
--- a/authentik/core/templates/login/base_full.html
+++ b/authentik/core/templates/login/base_full.html
@@ -12,7 +12,7 @@
{% block head %}
diff --git a/authentik/flows/templates/if/flow-sfe.html b/authentik/flows/templates/if/flow-sfe.html
index 39f528d416..311b3f139e 100644
--- a/authentik/flows/templates/if/flow-sfe.html
+++ b/authentik/flows/templates/if/flow-sfe.html
@@ -23,7 +23,7 @@
height: 100%;
}
body {
- background-image: url("{{ flow_background_url }}");
+ background-image: url("{{ flow_background_url|iriencode|safe }}");
background-repeat: no-repeat;
background-size: cover;
}
diff --git a/authentik/flows/templates/if/flow.html b/authentik/flows/templates/if/flow.html
index ea5d0f60cb..848e535040 100644
--- a/authentik/flows/templates/if/flow.html
+++ b/authentik/flows/templates/if/flow.html
@@ -39,7 +39,7 @@
{% endblock %}
diff --git a/authentik/flows/tests/test_stage_views.py b/authentik/flows/tests/test_stage_views.py
index a7379c7498..458d237803 100644
--- a/authentik/flows/tests/test_stage_views.py
+++ b/authentik/flows/tests/test_stage_views.py
@@ -1,12 +1,14 @@
"""stage view tests"""
from collections.abc import Callable
+from unittest.mock import patch
from django.test import RequestFactory, TestCase
+from django.urls import reverse
from authentik.core.tests.utils import RequestFactory as AuthentikRequestFactory
from authentik.core.tests.utils import create_test_flow
-from authentik.flows.models import FlowStageBinding
+from authentik.flows.models import Flow, FlowStageBinding
from authentik.flows.stage import StageView
from authentik.flows.views.executor import FlowExecutorView
from authentik.lib.utils.reflection import all_subclasses
@@ -42,6 +44,46 @@ class TestViews(TestCase):
"/static/dist/assets/images/flow_background.jpg",
)
+ def test_flow_interface_css_background_preserves_presigned_url_query(self):
+ """Test flow CSS keeps signed URL query separators intact."""
+ flow = create_test_flow()
+ background_url = (
+ "https://s3.ca-central-1.amazonaws.com/example/media/public/background.png"
+ "?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=credential"
+ "&X-Amz-Signature=signature"
+ )
+
+ with patch.object(Flow, "background_url", return_value=background_url):
+ response = self.client.get(
+ reverse("authentik_core:if-flow", kwargs={"flow_slug": flow.slug})
+ )
+
+ self.assertContains(
+ response,
+ f'--ak-global--background-image: url("{background_url}");',
+ html=False,
+ )
+
+ def test_flow_sfe_css_background_preserves_presigned_url_query(self):
+ """Test SFE flow CSS keeps signed URL query separators intact."""
+ flow = create_test_flow()
+ background_url = (
+ "https://s3.ca-central-1.amazonaws.com/example/media/public/background.png"
+ "?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=credential"
+ "&X-Amz-Signature=signature"
+ )
+
+ with patch.object(Flow, "background_url", return_value=background_url):
+ response = self.client.get(
+ reverse("authentik_core:if-flow", kwargs={"flow_slug": flow.slug}) + "?sfe"
+ )
+
+ self.assertContains(
+ response,
+ f'background-image: url("{background_url}");',
+ html=False,
+ )
+
def view_tester_factory(view_class: type[StageView]) -> Callable:
"""Test a form"""