Skip to content

Commit

Permalink
stages/email: Fix query parameters getting lost in Email links (#5376)
Browse files Browse the repository at this point in the history
* fix to email confirmation flow

* handled query keyerror

* rewrite using django's QueryDict, add tests

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* fix makefile

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* fix lint

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* remove commented out code

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Roney Dsilva <roney.dsilva@cdmx.in>
Co-authored-by: Jens Langhammer <jens@goauthentik.io>
  • Loading branch information
3 people committed Oct 19, 2023
1 parent acad3c4 commit f036820
Show file tree
Hide file tree
Showing 3 changed files with 121 additions and 10 deletions.
6 changes: 3 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,9 @@ test: ## Run the server tests and produce a coverage report (locally)
coverage report

lint-fix: ## Lint and automatically fix errors in the python source code. Reports spelling errors.
isort authentik $(PY_SOURCES)
black authentik $(PY_SOURCES)
ruff authentik $(PY_SOURCES)
isort $(PY_SOURCES)
black $(PY_SOURCES)
ruff $(PY_SOURCES)
codespell -w $(CODESPELL_ARGS)

lint: ## Lint the python and golang sources
Expand Down
23 changes: 19 additions & 4 deletions authentik/stages/email/stage.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@

from django.contrib import messages
from django.http import HttpRequest, HttpResponse
from django.http.request import QueryDict
from django.urls import reverse
from django.utils.http import urlencode
from django.utils.text import slugify
from django.utils.timezone import now
from django.utils.translation import gettext as _
Expand All @@ -15,7 +15,7 @@
from authentik.flows.models import FlowDesignation, FlowToken
from authentik.flows.planner import PLAN_CONTEXT_IS_RESTORED, PLAN_CONTEXT_PENDING_USER
from authentik.flows.stage import ChallengeStageView
from authentik.flows.views.executor import QS_KEY_TOKEN
from authentik.flows.views.executor import QS_KEY_TOKEN, QS_QUERY
from authentik.stages.email.models import EmailStage
from authentik.stages.email.tasks import send_mails
from authentik.stages.email.utils import TemplateEmailMessage
Expand Down Expand Up @@ -51,8 +51,22 @@ def get_full_url(self, **kwargs) -> str:
"authentik_core:if-flow",
kwargs={"flow_slug": self.executor.flow.slug},
)
relative_url = f"{base_url}?{urlencode(kwargs)}"
return self.request.build_absolute_uri(relative_url)
# Parse query string from current URL (full query string)
query_params = QueryDict(self.request.META.get("QUERY_STRING", ""), 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)
full_url = base_url
if len(query_params) > 0:
full_url = f"{full_url}?{query_params.urlencode()}"
return self.request.build_absolute_uri(full_url)

def get_token(self) -> FlowToken:
"""Get token"""
Expand Down Expand Up @@ -146,6 +160,7 @@ def challenge_invalid(self, response: ChallengeResponse) -> HttpResponse:
messages.error(self.request, _("No pending user."))
return super().challenge_invalid(response)
self.send_email()
messages.success(self.request, _("Email Successfully sent."))
# We can't call stage_ok yet, as we're still waiting
# for the user to click the link in the email
return super().challenge_invalid(response)
102 changes: 99 additions & 3 deletions authentik/stages/email/tests/test_stage.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from django.core import mail
from django.core.mail.backends.locmem import EmailBackend
from django.core.mail.backends.smtp import EmailBackend as SMTPEmailBackend
from django.test import RequestFactory
from django.urls import reverse
from django.utils.http import urlencode

Expand All @@ -12,19 +13,20 @@
from authentik.flows.models import FlowDesignation, FlowStageBinding, FlowToken
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan
from authentik.flows.tests import FlowTestCase
from authentik.flows.views.executor import QS_KEY_TOKEN, SESSION_KEY_PLAN
from authentik.flows.views.executor import QS_KEY_TOKEN, SESSION_KEY_PLAN, FlowExecutorView
from authentik.lib.config import CONFIG
from authentik.lib.generators import generate_id
from authentik.stages.email.models import EmailStage
from authentik.stages.email.stage import PLAN_CONTEXT_EMAIL_OVERRIDE
from authentik.stages.email.stage import PLAN_CONTEXT_EMAIL_OVERRIDE, EmailStageView


class TestEmailStage(FlowTestCase):
"""Email tests"""

def setUp(self):
super().setUp()
self.factory = RequestFactory()
self.user = create_test_admin_user()

self.flow = create_test_flow(FlowDesignation.AUTHENTICATION)
self.stage = EmailStage.objects.create(
name="email",
Expand Down Expand Up @@ -205,3 +207,97 @@ def test_token_invalid_user(self):

self.assertEqual(response.status_code, 200)
self.assertStageResponse(response, component="ak-stage-access-denied")

def test_url_no_params(self):
"""Test generation of the URL in the EMail"""
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})
request = self.factory.get(url)
stage_view = EmailStageView(
FlowExecutorView(
request=request,
flow=self.flow,
),
request=request,
)
self.assertEqual(stage_view.get_full_url(), f"http://testserver/if/flow/{self.flow.slug}/")

def test_url_our_params(self):
"""Test that all of our parameters are passed to the URL correctly"""
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})
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}/?flow_token={token}",
)

def test_url_existing_params(self):
"""Test to ensure that URL params are preserved in the URL being sent"""
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"
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}/?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}"
),
)

0 comments on commit f036820

Please sign in to comment.