Repository: goauthentik/authentik

Issue:
Email stage links incorrectly encoded
**Describe the bug**
Links sent in emails are getting doubly encoded.

For example:
`http://localhost:9000/if/flow/signup/?query=&amp;flow_token=W2kRqeEmVIdh0h...`

The issue is the `&` between querystring parameters.

**To Reproduce**
Steps to reproduce the behavior:

I'm seeing these on all signup flows and reset password flows where an email is triggered. 

**Expected behavior**
I'd expect the above to instead be 
`http://localhost:9000/if/flow/signup/?query=&flow_token=W2kRqeEmVIdh0h...`

**Version and Deployment (please complete the following information):**

-   authentik version: 2023.10.2
-   Deployment: only tested so far in docker-compose




Discussion:

Resolving patch:
diff --git a/authentik/stages/email/stage.py b/authentik/stages/email/stage.py
--- a/authentik/stages/email/stage.py
+++ b/authentik/stages/email/stage.py
@@ -52,17 +52,13 @@ def get_full_url(self, **kwargs) -> str:
             kwargs={"flow_slug": self.executor.flow.slug},
         )
         # Parse query string from current URL (full query string)
-        query_params = QueryDict(self.request.META.get("QUERY_STRING", ""), mutable=True)
+        # this view is only run within a flow executor, where we need to get the query string
+        # from the query= parameter (double encoded); but for the redirect
+        # we need to expand it since it'll go through the flow interface
+        query_params = QueryDict(self.request.GET.get(QS_QUERY), mutable=True)
         query_params.pop(QS_KEY_TOKEN, None)
-
-        # Check for nested query string used by flow executor, and remove any
-        # kind of flow token from that
-        if QS_QUERY in query_params:
-            inner_query_params = QueryDict(query_params.get(QS_QUERY), mutable=True)
-            inner_query_params.pop(QS_KEY_TOKEN, None)
-            query_params[QS_QUERY] = inner_query_params.urlencode()
-
         query_params.update(kwargs)
+        print(query_params)
         full_url = base_url
         if len(query_params) > 0:
             full_url = f"{full_url}?{query_params.urlencode()}"


Test patch:
diff --git a/authentik/stages/email/tests/test_stage.py b/authentik/stages/email/tests/test_stage.py
--- a/authentik/stages/email/tests/test_stage.py
+++ b/authentik/stages/email/tests/test_stage.py
@@ -259,7 +259,7 @@ def test_url_existing_params(self):
         session.save()
 
         url = reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug})
-        url += "?foo=bar"
+        url += "?query=" + urlencode({"foo": "bar"})
         request = self.factory.get(url)
         stage_view = EmailStageView(
             FlowExecutorView(
@@ -273,31 +273,3 @@ def test_url_existing_params(self):
             stage_view.get_full_url(**{QS_KEY_TOKEN: token}),
             f"http://testserver/if/flow/{self.flow.slug}/?foo=bar&flow_token={token}",
         )
-
-    def test_url_existing_params_nested(self):
-        """Test to ensure that URL params are preserved in the URL being sent (including nested)"""
-        plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])
-        plan.context[PLAN_CONTEXT_PENDING_USER] = self.user
-        session = self.client.session
-        session[SESSION_KEY_PLAN] = plan
-        session.save()
-
-        url = reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug})
-        url += "?foo=bar&"
-        url += "query=" + urlencode({"nested": "value"})
-        request = self.factory.get(url)
-        stage_view = EmailStageView(
-            FlowExecutorView(
-                request=request,
-                flow=self.flow,
-            ),
-            request=request,
-        )
-        token = generate_id()
-        self.assertEqual(
-            stage_view.get_full_url(**{QS_KEY_TOKEN: token}),
-            (
-                f"http://testserver/if/flow/{self.flow.slug}"
-                f"/?foo=bar&query=nested%3Dvalue&flow_token={token}"
-            ),
-        )