Repository: goauthentik/authentik

Issue:
MFA Flow "Request Denied"
Recreating this using the proper format:

**Describe the bug**
I have created a very basic flow that consists only of a MFA verification stage. There are no policies bound to the flow or the stage. The flow is used with an OAuth2 provider to require users to enter a code in order to launch the app. However, when the user enters their code, instead of being taken to the application, they are shown a "Request Denied" screen, with "Unknown Error" shown at the bottom. With a log level of trace set, there is no Traceback generated.

**To Reproduce**
Steps to reproduce the behavior:
1. Create a flow that consists only of an MFA validation stage. No policy, user, or group bindings are needed
2. Assign that flow to an application
3. Try to access that application using a valid MFA code (with an account with an authenticator configured)
4. See error

**Expected behavior**
The MFA token would be accepted and the application would be launched/redirected to

**Screenshots**
<img width="581" alt="Screen Shot 2023-03-27 at 11 40 01 AM" src="https://user-images.githubusercontent.com/25728719/227991816-bf2740ac-1f98-4896-961f-22e5fd3e479b.png">

**Logs**
[authentiklogs.txt](https://github.com/goauthentik/authentik/files/11080519/authentiklogs.txt)
To me the logs do not show anything out of the ordinary.

**Version and Deployment (please complete the following information):**
 - authentik version: 2023.3.1
 - Deployment: docker-compose



Discussion:

Resolving patch:
diff --git a/authentik/stages/authenticator_validate/stage.py b/authentik/stages/authenticator_validate/stage.py
--- a/authentik/stages/authenticator_validate/stage.py
+++ b/authentik/stages/authenticator_validate/stage.py
@@ -377,9 +377,7 @@ def set_valid_mfa_cookie(self, device: Device) -> HttpResponse:
     def challenge_valid(self, response: AuthenticatorValidationChallengeResponse) -> HttpResponse:
         # All validation is done by the serializer
         user = self.executor.plan.context.get(PLAN_CONTEXT_PENDING_USER)
-        if not user:
-            if "webauthn" not in response.data:
-                return self.executor.stage_invalid()
+        if not user and "webauthn" in response.data:
             webauthn_device: WebAuthnDevice = response.device
             self.logger.debug("Set user from user-less flow", user=webauthn_device.user)
             self.executor.plan.context[PLAN_CONTEXT_PENDING_USER] = webauthn_device.user


Test patch:
diff --git a/authentik/stages/authenticator_validate/tests/test_stage.py b/authentik/stages/authenticator_validate/tests/test_stage.py
--- a/authentik/stages/authenticator_validate/tests/test_stage.py
+++ b/authentik/stages/authenticator_validate/tests/test_stage.py
@@ -1,18 +1,22 @@
 """Test validator stage"""
+from unittest.mock import MagicMock, patch
+
 from django.contrib.sessions.middleware import SessionMiddleware
 from django.test.client import RequestFactory
 from django.urls.base import reverse
 from rest_framework.exceptions import ValidationError
 
 from authentik.core.tests.utils import create_test_admin_user, create_test_flow
-from authentik.flows.models import FlowStageBinding, NotConfiguredAction
+from authentik.flows.models import FlowDesignation, FlowStageBinding, NotConfiguredAction
+from authentik.flows.planner import FlowPlan
 from authentik.flows.stage import StageView
 from authentik.flows.tests import FlowTestCase
-from authentik.flows.views.executor import FlowExecutorView
-from authentik.lib.generators import generate_id
+from authentik.flows.views.executor import SESSION_KEY_PLAN, FlowExecutorView
+from authentik.lib.generators import generate_id, generate_key
 from authentik.lib.tests.utils import dummy_get_response
+from authentik.stages.authenticator_duo.models import AuthenticatorDuoStage, DuoDevice
 from authentik.stages.authenticator_validate.api import AuthenticatorValidateStageSerializer
-from authentik.stages.authenticator_validate.models import AuthenticatorValidateStage
+from authentik.stages.authenticator_validate.models import AuthenticatorValidateStage, DeviceClasses
 from authentik.stages.authenticator_validate.stage import (
     SESSION_KEY_DEVICE_CHALLENGES,
     AuthenticatorValidationChallengeResponse,
@@ -115,3 +119,57 @@ def test_validate_selected_challenge(self):
                 "device_uid": "1",
             }
         )
+
+    @patch(
+        "authentik.stages.authenticator_duo.models.AuthenticatorDuoStage.auth_client",
+        MagicMock(
+            return_value=MagicMock(
+                auth=MagicMock(
+                    return_value={
+                        "result": "allow",
+                        "status": "allow",
+                        "status_msg": "Success. Logging you in...",
+                    }
+                )
+            )
+        ),
+    )
+    def test_non_authentication_flow(self):
+        """Test full in an authorization flow (no pending user)"""
+        self.client.force_login(self.user)
+        duo_stage = AuthenticatorDuoStage.objects.create(
+            name=generate_id(),
+            client_id=generate_id(),
+            client_secret=generate_key(),
+            api_hostname="",
+        )
+        duo_device = DuoDevice.objects.create(
+            user=self.user,
+            stage=duo_stage,
+        )
+
+        flow = create_test_flow(FlowDesignation.AUTHORIZATION)
+        stage = AuthenticatorValidateStage.objects.create(
+            name=generate_id(),
+            device_classes=[DeviceClasses.DUO],
+        )
+
+        plan = FlowPlan(flow_pk=flow.pk.hex)
+        plan.append(FlowStageBinding.objects.create(target=flow, stage=stage, order=2))
+        session = self.client.session
+        session[SESSION_KEY_PLAN] = plan
+        session.save()
+
+        response = self.client.get(
+            reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}),
+        )
+        self.assertEqual(response.status_code, 200)
+
+        response = self.client.post(
+            reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}),
+            {"duo": duo_device.pk},
+            follow=True,
+        )
+
+        self.assertEqual(response.status_code, 200)
+        self.assertStageRedirects(response, reverse("authentik_core:root-redirect"))