Repository: goauthentik/authentik

Issue:
Custom Email Template raises exception, not sent; GET /api/v3/flows/executor/password-recovery/
**Describe the bug**
Version: 2023.10
I created the `email-templates` volume then dropped 'custom-email-template.html' containing the exact code from the example here: [Template Page](https://goauthentik.io/docs/flow/stages/email/) 

**To Reproduce**
See above

**Expected behavior**
An email is sent using the template

**Logs**
<details>
    <summary>Stacktrace from authentik</summary>

```
Traceback (most recent call last):
  File "/authentik/flows/views/executor.py", line 287, in get
    stage_response = self.current_stage_view.dispatch(request)
                     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/ak-root/venv/lib/python3.11/site-packages/django/views/generic/base.py", line 143, in dispatch
    return handler(request, *args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/authentik/stages/email/stage.py", line 139, in get
    self.send_email()
  File "/authentik/stages/email/stage.py", line 107, in send_email
    message = TemplateEmailMessage(
              ^^^^^^^^^^^^^^^^^^^^^
  File "/authentik/stages/email/utils.py", line 28, in __init__
    html_content = render_to_string(template_name, template_context)
                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/ak-root/venv/lib/python3.11/site-packages/django/template/loader.py", line 61, in render_to_string
    template = get_template(template_name, using=using)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/ak-root/venv/lib/python3.11/site-packages/django/template/loader.py", line 15, in get_template
    return engine.get_template(template_name)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/ak-root/venv/lib/python3.11/site-packages/django/template/backends/django.py", line 33, in get_template
    return Template(self.engine.get_template(template_name), self)
                    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/ak-root/venv/lib/python3.11/site-packages/django/template/engine.py", line 175, in get_template
    template, origin = self.find_template(template_name)
                       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/ak-root/venv/lib/python3.11/site-packages/django/template/engine.py", line 157, in find_template
    template = loader.get_template(name, skip=skip)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/ak-root/venv/lib/python3.11/site-packages/django/template/loaders/cached.py", line 57, in get_template
    template = super().get_template(template_name, skip)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/ak-root/venv/lib/python3.11/site-packages/django/template/loaders/base.py", line 28, in get_template
    return Template(
           ^^^^^^^^^
  File "/ak-root/venv/lib/python3.11/site-packages/django/template/base.py", line 154, in __init__
    self.nodelist = self.compile_nodelist()
                    ^^^^^^^^^^^^^^^^^^^^^^^
  File "/ak-root/venv/lib/python3.11/site-packages/django/template/base.py", line 200, in compile_nodelist
    return parser.parse()
           ^^^^^^^^^^^^^^
  File "/ak-root/venv/lib/python3.11/site-packages/django/template/base.py", line 513, in parse
    raise self.error(token, e)
  File "/ak-root/venv/lib/python3.11/site-packages/django/template/base.py", line 511, in parse
    compiled_result = compile_func(self, token)
                      ^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/ak-root/venv/lib/python3.11/site-packages/django/templatetags/i18n.py", line 581, in do_block_translate
    raise TemplateSyntaxError(
django.template.exceptions.TemplateSyntaxError: 'blocktrans' doesn't allow other block tags (seen 'blocktrans') inside it
```
<details>
   <summary>Python Traceback</summary>

```
django.template.exceptions.TemplateSyntaxError: 'blocktrans' doesn't allow other block tags (seen 'blocktrans') inside it

Show less
Context
{
    "message": "Traceback (most recent call last):\n  File \"/authentik/flows/views/executor.py\", line 287, in get\n    stage_response = self.current_stage_view.dispatch(request)\n                     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n  File \"/ak-root/venv/lib/python3.11/site-packages/django/views/generic/base.py\", line 143, in dispatch\n    return handler(request, *args, **kwargs)\n           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n  File \"/authentik/stages/email/stage.py\", line 139, in get\n    self.send_email()\n  File \"/authentik/stages/email/stage.py\", line 107, in send_email\n    message = TemplateEmailMessage(\n              ^^^^^^^^^^^^^^^^^^^^^\n  File \"/authentik/stages/email/utils.py\", line 28, in __init__\n    html_content = render_to_string(template_name, template_context)\n                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n  File \"/ak-root/venv/lib/python3.11/site-packages/django/template/loader.py\", line 61, in render_to_string\n    template = get_template(template_name, using=using)\n               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n  File \"/ak-root/venv/lib/python3.11/site-packages/django/template/loader.py\", line 15, in get_template\n    return engine.get_template(template_name)\n           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n  File \"/ak-root/venv/lib/python3.11/site-packages/django/template/backends/django.py\", line 33, in get_template\n    return Template(self.engine.get_template(template_name), self)\n                    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n  File \"/ak-root/venv/lib/python3.11/site-packages/django/template/engine.py\", line 175, in get_template\n    template, origin = self.find_template(template_name)\n                       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n  File \"/ak-root/venv/lib/python3.11/site-packages/django/template/engine.py\", line 157, in find_template\n    template = loader.get_template(name, skip=skip)\n               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n  File \"/ak-root/venv/lib/python3.11/site-packages/django/template/loaders/cached.py\", line 57, in get_template\n    template = super().get_template(template_name, skip)\n               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n  File \"/ak-root/venv/lib/python3.11/site-packages/django/template/loaders/base.py\", line 28, in get_template\n    return Template(\n           ^^^^^^^^^\n  File \"/ak-root/venv/lib/python3.11/site-packages/django/template/base.py\", line 154, in __init__\n    self.nodelist = self.compile_nodelist()\n                    ^^^^^^^^^^^^^^^^^^^^^^^\n  File \"/ak-root/venv/lib/python3.11/site-packages/django/template/base.py\", line 200, in compile_nodelist\n    return parser.parse()\n           ^^^^^^^^^^^^^^\n  File \"/ak-root/venv/lib/python3.11/site-packages/django/template/base.py\", line 513, in parse\n    raise self.error(token, e)\n  File \"/ak-root/venv/lib/python3.11/site-packages/django/template/base.py\", line 511, in parse\n    compiled_result = compile_func(self, token)\n                      ^^^^^^^^^^^^^^^^^^^^^^^^^\n  File \"/ak-root/venv/lib/python3.11/site-packages/django/templatetags/i18n.py\", line 581, in do_block_translate\n    raise TemplateSyntaxError(\ndjango.template.exceptions.TemplateSyntaxError: 'blocktrans' doesn't allow other block tags (seen 'blocktrans') inside it",
    "http_request": {
        "args": {
            "next": "/"
        },
        "path": "/api/v3/flows/executor/password-recovery/",
        "method": "GET"
    }
}
User
{
    "pk": 1,
    "email": "",
    "username": "AnonymousUser"
}
```

</details>

</details>




**Version and Deployment (please complete the following information):**
- authentik version: 2023.10.4
- Deployment: [e.g. docker-compose, helm]

**Additional context**
Add any other context about the problem here.
        


Discussion:

Resolving patch:
diff --git a/authentik/core/api/sources.py b/authentik/core/api/sources.py
--- a/authentik/core/api/sources.py
+++ b/authentik/core/api/sources.py
@@ -38,7 +38,7 @@ class SourceSerializer(ModelSerializer, MetaNameSerializer):
 
     managed = ReadOnlyField()
     component = SerializerMethodField()
-    icon = ReadOnlyField(source="get_icon")
+    icon = ReadOnlyField(source="icon_url")
 
     def get_component(self, obj: Source) -> str:
         """Get object component so that we know how to edit the object"""
diff --git a/authentik/flows/stage.py b/authentik/flows/stage.py
--- a/authentik/flows/stage.py
+++ b/authentik/flows/stage.py
@@ -167,7 +167,11 @@ def _get_challenge(self, *args, **kwargs) -> Challenge:
                 stage_type=self.__class__.__name__, method="get_challenge"
             ).time(),
         ):
-            challenge = self.get_challenge(*args, **kwargs)
+            try:
+                challenge = self.get_challenge(*args, **kwargs)
+            except StageInvalidException as exc:
+                self.logger.debug("Got StageInvalidException", exc=exc)
+                return self.executor.stage_invalid()
         with Hub.current.start_span(
             op="authentik.flow.stage._get_challenge",
             description=self.__class__.__name__,
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
@@ -5,6 +5,7 @@
 from django.contrib import messages
 from django.http import HttpRequest, HttpResponse
 from django.http.request import QueryDict
+from django.template.exceptions import TemplateSyntaxError
 from django.urls import reverse
 from django.utils.text import slugify
 from django.utils.timezone import now
@@ -12,11 +13,14 @@
 from rest_framework.fields import CharField
 from rest_framework.serializers import ValidationError
 
+from authentik.events.models import Event, EventAction
 from authentik.flows.challenge import Challenge, ChallengeResponse, ChallengeTypes
+from authentik.flows.exceptions import StageInvalidException
 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, QS_QUERY
+from authentik.lib.utils.errors import exception_to_string
 from authentik.stages.email.models import EmailStage
 from authentik.stages.email.tasks import send_mails
 from authentik.stages.email.utils import TemplateEmailMessage
@@ -103,18 +107,27 @@ def send_email(self):
         current_stage: EmailStage = self.executor.current_stage
         token = self.get_token()
         # Send mail to user
-        message = TemplateEmailMessage(
-            subject=_(current_stage.subject),
-            to=[email],
-            language=pending_user.locale(self.request),
-            template_name=current_stage.template,
-            template_context={
-                "url": self.get_full_url(**{QS_KEY_TOKEN: token.key}),
-                "user": pending_user,
-                "expires": token.expires,
-            },
-        )
-        send_mails(current_stage, message)
+        try:
+            message = TemplateEmailMessage(
+                subject=_(current_stage.subject),
+                to=[email],
+                language=pending_user.locale(self.request),
+                template_name=current_stage.template,
+                template_context={
+                    "url": self.get_full_url(**{QS_KEY_TOKEN: token.key}),
+                    "user": pending_user,
+                    "expires": token.expires,
+                },
+            )
+            send_mails(current_stage, message)
+        except TemplateSyntaxError as exc:
+            Event.new(
+                EventAction.CONFIGURATION_ERROR,
+                message=_("Exception occurred while rendering E-mail template"),
+                error=exception_to_string(exc),
+                template=current_stage.template,
+            ).from_http(self.request)
+            raise StageInvalidException from exc
 
     def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
         # Check if the user came back from the email link to verify
@@ -135,7 +148,11 @@ def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
             return self.executor.stage_invalid()
         # Check if we've already sent the initial e-mail
         if PLAN_CONTEXT_EMAIL_SENT not in self.executor.plan.context:
-            self.send_email()
+            try:
+                self.send_email()
+            except StageInvalidException as exc:
+                self.logger.debug("Got StageInvalidException", exc=exc)
+                return self.executor.stage_invalid()
             self.executor.plan.context[PLAN_CONTEXT_EMAIL_SENT] = True
         return super().get(request, *args, **kwargs)
 


Test patch:
diff --git a/authentik/stages/email/tests/test_templates.py b/authentik/stages/email/tests/test_templates.py
--- a/authentik/stages/email/tests/test_templates.py
+++ b/authentik/stages/email/tests/test_templates.py
@@ -4,11 +4,20 @@
 from shutil import rmtree
 from tempfile import mkdtemp, mkstemp
 from typing import Any
+from unittest.mock import PropertyMock, patch
 
 from django.conf import settings
-from django.test import TestCase
+from django.core.mail.backends.locmem import EmailBackend
+from django.urls import reverse
 
-from authentik.stages.email.models import get_template_choices
+from authentik.core.tests.utils import create_test_admin_user, create_test_flow
+from authentik.events.models import Event, EventAction
+from authentik.flows.markers import StageMarker
+from authentik.flows.models import FlowDesignation, FlowStageBinding
+from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan
+from authentik.flows.tests import FlowTestCase
+from authentik.flows.views.executor import SESSION_KEY_PLAN
+from authentik.stages.email.models import EmailStage, get_template_choices
 
 
 def get_templates_setting(temp_dir: str) -> dict[str, Any]:
@@ -18,11 +27,18 @@ def get_templates_setting(temp_dir: str) -> dict[str, Any]:
     return templates_setting
 
 
-class TestEmailStageTemplates(TestCase):
+class TestEmailStageTemplates(FlowTestCase):
     """Email tests"""
 
     def setUp(self) -> None:
-        self.dir = mkdtemp()
+        self.dir = Path(mkdtemp())
+        self.user = create_test_admin_user()
+
+        self.flow = create_test_flow(FlowDesignation.AUTHENTICATION)
+        self.stage = EmailStage.objects.create(
+            name="email",
+        )
+        self.binding = FlowStageBinding.objects.create(target=self.flow, stage=self.stage, order=2)
 
     def tearDown(self) -> None:
         rmtree(self.dir)
@@ -38,3 +54,37 @@ def test_custom_template(self):
             self.assertEqual(len(choices), 3)
             unlink(file)
             unlink(file2)
+
+    def test_custom_template_invalid_syntax(self):
+        """Test with custom template"""
+        with open(self.dir / Path("invalid.html"), "w+", encoding="utf-8") as _invalid:
+            _invalid.write("{% blocktranslate %}")
+        with self.settings(TEMPLATES=get_templates_setting(self.dir)):
+            self.stage.template = "invalid.html"
+            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})
+            with patch(
+                "authentik.stages.email.models.EmailStage.backend_class",
+                PropertyMock(return_value=EmailBackend),
+            ):
+                response = self.client.get(url)
+                self.assertEqual(response.status_code, 200)
+                self.assertStageResponse(
+                    response,
+                    self.flow,
+                    error_message="Unknown error",
+                )
+                events = Event.objects.filter(action=EventAction.CONFIGURATION_ERROR)
+                self.assertEqual(len(events), 1)
+                event = events.first()
+                self.assertEqual(
+                    event.context["message"], "Exception occurred while rendering E-mail template"
+                )
+                self.assertEqual(event.context["template"], "invalid.html")