def test_with_invitation_prompt_data(self): """Test with invitation, check data in session""" data = {"foo": "bar"} invite = Invitation.objects.create(created_by=get_anonymous_user(), fixed_data=data, single_use=True) plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()]) plan.context[PLAN_CONTEXT_PROMPT] = { INVITATION_TOKEN_KEY_CONTEXT: invite.pk.hex } session = self.client.session session[SESSION_KEY_PLAN] = plan session.save() with patch("authentik.flows.views.executor.FlowExecutorView.cancel", MagicMock()): base_url = reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}) response = self.client.get(base_url, follow=True) session = self.client.session plan: FlowPlan = session[SESSION_KEY_PLAN] self.assertEqual(plan.context[PLAN_CONTEXT_PROMPT], data | plan.context[PLAN_CONTEXT_PROMPT]) self.assertEqual(response.status_code, 200) self.assertStageRedirects(response, reverse("authentik_core:root-redirect")) self.assertFalse(Invitation.objects.filter(pk=invite.pk))
def test_user_update(self): """Test update of existing user""" new_password = "".join(SystemRandom().choice(string.ascii_uppercase + string.digits) for _ in range(8)) plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()]) plan.context[PLAN_CONTEXT_PENDING_USER] = User.objects.create( username="******", email="*****@*****.**") plan.context[PLAN_CONTEXT_PROMPT] = { "username": "******", "password": new_password, "attribute.some.custom-attribute": "test", "some_ignored_attribute": "bar", } session = self.client.session session[SESSION_KEY_PLAN] = plan session.save() response = self.client.post( reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug})) self.assertEqual(response.status_code, 200) self.assertStageRedirects(response, reverse("authentik_core:root-redirect")) user_qs = User.objects.filter( username=plan.context[PLAN_CONTEXT_PROMPT]["username"]) self.assertTrue(user_qs.exists()) self.assertTrue(user_qs.first().check_password(new_password)) self.assertEqual( user_qs.first().attributes["some"]["custom-attribute"], "test") self.assertNotIn("some_ignored_attribute", user_qs.first().attributes)
def test_send_error(self): """Test error during sending (sending will be retried)""" plan = FlowPlan(flow_pk=self.flow.pk.hex, stages=[self.stage], 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 self.settings( EMAIL_BACKEND="django.core.mail.backends.locmem.EmailBackend"): with patch( "django.core.mail.backends.locmem.EmailBackend.send_messages", MagicMock(side_effect=[ SMTPException, EmailBackend.send_messages ]), ): response = self.client.post(url) response = self.client.post(url) self.assertEqual(response.status_code, 200) self.assertTrue(len(mail.outbox) >= 1) self.assertEqual(mail.outbox[0].subject, "authentik")
def test_permission_denied(self): """Test with a valid pending user and valid password. Backend is patched to return PermissionError""" 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() response = self.client.post( reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}), # Form data {"password": self.password + "test"}, ) self.assertEqual(response.status_code, 200) self.assertStageResponse( response, self.flow, component="ak-stage-access-denied", error_message="Unknown error", )
def test_with_blank_username(self): """Test with blank username results in error""" plan = FlowPlan(flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()]) session = self.client.session plan.context[PLAN_CONTEXT_PROMPT] = { "username": "", "attribute_some-custom-attribute": "test", "some_ignored_attribute": "bar", } session[SESSION_KEY_PLAN] = plan session.save() response = self.client.get( reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug})) self.assertEqual(response.status_code, 200) self.assertJSONEqual( force_str(response.content), { "component": "ak-stage-access-denied", "error_message": None, "title": "", "type": ChallengeTypes.NATIVE.value, }, )
def test_permission_denied(self): """Test with a valid pending user and valid password. Backend is patched to return PermissionError""" plan = FlowPlan(flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()]) plan.context[PLAN_CONTEXT_PENDING_USER] = self.user session = self.client.session session[SESSION_KEY_PLAN] = plan session.save() response = self.client.post( reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}), # Form data {"password": self.password + "test"}, ) self.assertEqual(response.status_code, 200) self.assertJSONEqual( force_str(response.content), { "component": "ak-stage-access-denied", "error_message": None, "title": "", "type": ChallengeTypes.NATIVE.value, }, )
def test_without_invitation_continue(self): """Test without any invitation, continue_flow_without_invitation is set.""" self.stage.continue_flow_without_invitation = True self.stage.save() plan = FlowPlan( flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()] ) plan.context[PLAN_CONTEXT_PENDING_USER] = self.user plan.context[ PLAN_CONTEXT_AUTHENTICATION_BACKEND ] = "django.contrib.auth.backends.ModelBackend" session = self.client.session session[SESSION_KEY_PLAN] = plan session.save() response = self.client.get( reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}) ) self.assertEqual(response.status_code, 200) self.assertJSONEqual( force_str(response.content), { "component": "xak-flow-redirect", "to": reverse("authentik_core:root-redirect"), "type": ChallengeTypes.REDIRECT.value, }, ) self.stage.continue_flow_without_invitation = False self.stage.save()
def test_with_invitation_prompt_data(self): """Test with invitation, check data in session""" data = {"foo": "bar"} invite = Invitation.objects.create( created_by=get_anonymous_user(), fixed_data=data, single_use=True ) plan = FlowPlan( flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()] ) plan.context[PLAN_CONTEXT_PROMPT] = {INVITATION_TOKEN_KEY: invite.pk.hex} session = self.client.session session[SESSION_KEY_PLAN] = plan session.save() with patch("authentik.flows.views.FlowExecutorView.cancel", MagicMock()): base_url = reverse( "authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug} ) response = self.client.get(base_url) session = self.client.session plan: FlowPlan = session[SESSION_KEY_PLAN] self.assertEqual(plan.context[PLAN_CONTEXT_PROMPT], data) self.assertEqual(response.status_code, 200) self.assertJSONEqual( force_str(response.content), { "component": "xak-flow-redirect", "to": reverse("authentik_core:root-redirect"), "type": ChallengeTypes.REDIRECT.value, }, ) self.assertFalse(Invitation.objects.filter(pk=invite.pk))
def test_without_invitation_fail(self): """Test without any invitation, continue_flow_without_invitation not set.""" plan = FlowPlan( flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()] ) plan.context[PLAN_CONTEXT_PENDING_USER] = self.user plan.context[ PLAN_CONTEXT_AUTHENTICATION_BACKEND ] = "django.contrib.auth.backends.ModelBackend" session = self.client.session session[SESSION_KEY_PLAN] = plan session.save() response = self.client.get( reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}) ) self.assertEqual(response.status_code, 200) self.assertJSONEqual( force_str(response.content), { "component": "ak-stage-access-denied", "error_message": None, "title": "", "type": ChallengeTypes.NATIVE.value, }, )
def test_user_create(self): """Test creation of user""" password = "".join(SystemRandom().choice(string.ascii_uppercase + string.digits) for _ in range(8)) plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()]) plan.context[PLAN_CONTEXT_PROMPT] = { "username": "******", "name": "name", "email": "*****@*****.**", "password": password, } plan.context[PLAN_CONTEXT_SOURCES_CONNECTION] = UserSourceConnection( source=self.source) session = self.client.session session[SESSION_KEY_PLAN] = plan session.save() response = self.client.get( reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug})) self.assertEqual(response.status_code, 200) self.assertStageRedirects(response, reverse("authentik_core:root-redirect")) user_qs = User.objects.filter( username=plan.context[PLAN_CONTEXT_PROMPT]["username"]) self.assertTrue(user_qs.exists()) self.assertTrue(user_qs.first().check_password(password)) self.assertEqual(list(user_qs.first().ak_groups.all()), [self.group]) self.assertEqual(user_qs.first().attributes, {USER_ATTRIBUTE_SOURCES: [self.source.name]})
def test_invalid_password_lockout(self): """Test with a valid pending user and invalid password (trigger logout counter)""" plan = FlowPlan(flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()]) plan.context[PLAN_CONTEXT_PENDING_USER] = self.user session = self.client.session session[SESSION_KEY_PLAN] = plan session.save() for _ in range(self.stage.failed_attempts_before_cancel): response = self.client.post( reverse( "authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}, ), # Form data {"password": self.password + "test"}, ) self.assertEqual(response.status_code, 200) response = self.client.post( reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}), # Form data {"password": self.password + "test"}, ) self.assertEqual(response.status_code, 200) # To ensure the plan has been cancelled, check SESSION_KEY_PLAN self.assertNotIn(SESSION_KEY_PLAN, self.client.session)
def test_user_create(self): """Test creation of user""" password = "".join(SystemRandom().choice(string.ascii_uppercase + string.digits) for _ in range(8)) plan = FlowPlan(flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()]) plan.context[PLAN_CONTEXT_PROMPT] = { "username": "******", "name": "name", "email": "*****@*****.**", "password": password, } session = self.client.session session[SESSION_KEY_PLAN] = plan session.save() response = self.client.get( reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug})) self.assertEqual(response.status_code, 200) self.assertJSONEqual( force_str(response.content), { "to": reverse("authentik_core:root-redirect"), "type": "redirect" }, ) user_qs = User.objects.filter( username=plan.context[PLAN_CONTEXT_PROMPT]["username"]) self.assertTrue(user_qs.exists()) self.assertTrue(user_qs.first().check_password(password))
def test_valid_password(self): """Test with a valid pending user and valid password""" plan = FlowPlan(flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()]) plan.context[PLAN_CONTEXT_PENDING_USER] = self.user session = self.client.session session[SESSION_KEY_PLAN] = plan session.save() response = self.client.post( reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}), # Form data {"password": self.password}, ) self.assertEqual(response.status_code, 200) self.assertJSONEqual( force_str(response.content), { "component": "xak-flow-redirect", "to": reverse("authentik_core:root-redirect"), "type": ChallengeTypes.REDIRECT.value, }, )
def test_duplicate_data(self): """Test with duplicate data, should trigger error""" user = create_test_admin_user() plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()]) session = self.client.session plan.context[PLAN_CONTEXT_PROMPT] = { "username": user.username, "attribute_some-custom-attribute": "test", "some_ignored_attribute": "bar", } session[SESSION_KEY_PLAN] = plan session.save() response = self.client.get( reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug})) self.assertEqual(response.status_code, 200) self.assertStageResponse( response, self.flow, component="ak-stage-access-denied", )
def test_expiry(self): """Test with expiry""" self.stage.session_duration = "seconds=2" self.stage.save() plan = FlowPlan(flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()]) plan.context[PLAN_CONTEXT_PENDING_USER] = self.user session = self.client.session session[SESSION_KEY_PLAN] = plan session.save() response = self.client.get( reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug})) self.assertEqual(response.status_code, 200) self.assertJSONEqual( force_str(response.content), { "to": reverse("authentik_core:root-redirect"), "type": "redirect" }, ) self.assertNotEqual(list(self.client.session.keys()), []) sleep(3) self.client.session.clear_expired() self.assertEqual(list(self.client.session.keys()), [])
def test_rendering(self): """Test with pending user""" plan = FlowPlan(flow_pk=self.flow.pk.hex, stages=[self.stage], 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}) response = self.client.get(url) self.assertEqual(response.status_code, 200)
def test_permanent(self): """Test permanent consent from user""" self.client.force_login(self.user) flow = Flow.objects.create( name="test-consent", slug="test-consent", designation=FlowDesignation.AUTHENTICATION, ) stage = ConsentStage.objects.create(name="consent", mode=ConsentMode.PERMANENT) FlowStageBinding.objects.create(target=flow, stage=stage, order=2) plan = FlowPlan( flow_pk=flow.pk.hex, stages=[stage], markers=[StageMarker()], context={PLAN_CONTEXT_APPLICATION: self.application}, ) session = self.client.session session[SESSION_KEY_PLAN] = plan session.save() response = self.client.post( reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}), {}, ) self.assertEqual(response.status_code, 200) self.assertJSONEqual( force_str(response.content), {"to": reverse("authentik_core:root-redirect"), "type": "redirect"}, ) self.assertTrue( UserConsent.objects.filter( user=self.user, application=self.application ).exists() )
def test_without_user(self): """Test without user""" plan = FlowPlan(flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()]) session = self.client.session session[SESSION_KEY_PLAN] = plan session.save() response = self.client.post( reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}), # Still have to send the password so the form is valid {"password": self.password}, ) self.assertEqual(response.status_code, 200) self.assertJSONEqual( force_str(response.content), { "component": "ak-stage-access-denied", "error_message": None, "title": "", "type": ChallengeTypes.NATIVE.value, }, )
def test_with_invitation_get(self): """Test with invitation, check data in session""" plan = FlowPlan(flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()]) session = self.client.session session[SESSION_KEY_PLAN] = plan session.save() data = {"foo": "bar"} invite = Invitation.objects.create(created_by=get_anonymous_user(), fixed_data=data) with patch("authentik.flows.views.FlowExecutorView.cancel", MagicMock()): base_url = reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}) args = urlencode({INVITATION_TOKEN_KEY: invite.pk.hex}) response = self.client.get(base_url + f"?query={args}") session = self.client.session plan: FlowPlan = session[SESSION_KEY_PLAN] self.assertEqual(plan.context[PLAN_CONTEXT_PROMPT], data) self.assertEqual(response.status_code, 200) self.assertJSONEqual( force_str(response.content), { "to": reverse("authentik_core:root-redirect"), "type": "redirect" }, )
def test_always_required(self): """Test always required consent""" flow = Flow.objects.create( name="test-consent", slug="test-consent", designation=FlowDesignation.AUTHENTICATION, ) stage = ConsentStage.objects.create(name="consent", mode=ConsentMode.ALWAYS_REQUIRE) FlowStageBinding.objects.create(target=flow, stage=stage, order=2) plan = FlowPlan(flow_pk=flow.pk.hex, stages=[stage], markers=[StageMarker()]) session = self.client.session session[SESSION_KEY_PLAN] = plan session.save() response = self.client.post( reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}), {}, ) self.assertEqual(response.status_code, 200) self.assertJSONEqual( force_str(response.content), { "component": "xak-flow-redirect", "to": reverse("authentik_core:root-redirect"), "type": ChallengeTypes.REDIRECT.value, }, ) self.assertFalse(UserConsent.objects.filter(user=self.user).exists())
def test_valid_challenge_request(self): """Test a request with valid challenge_response data""" plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()]) session = self.client.session session[SESSION_KEY_PLAN] = plan session.save() challenge_response = self.test_valid_challenge_with_policy() with patch("authentik.flows.views.executor.FlowExecutorView.cancel", MagicMock()): response = self.client.post( reverse( "authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}, ), challenge_response.validated_data, ) self.assertEqual(response.status_code, 200) self.assertStageRedirects(response, reverse("authentik_core:root-redirect")) # Check that valid data has been saved session = self.client.session plan: FlowPlan = session[SESSION_KEY_PLAN] data = plan.context[PLAN_CONTEXT_PROMPT] for prompt in self.stage.fields.all(): prompt: Prompt self.assertEqual(data[prompt.field_key], self.prompt_data[prompt.field_key])
def setUp(self) -> None: self.user = User.objects.create(username="******") self.token = Token.objects.create( expiring=False, user=self.user, intent=TokenIntents.INTENT_APP_PASSWORD ) # To test with session we need to create a request and pass it through all middlewares self.request = get_request("/") self.request.session[SESSION_KEY_PLAN] = FlowPlan("test")
def test_invalid_password(self): """Test with a valid pending user and invalid password""" plan = FlowPlan(flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()]) plan.context[PLAN_CONTEXT_PENDING_USER] = self.user session = self.client.session session[SESSION_KEY_PLAN] = plan session.save() response = self.client.post( reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}), # Form data {"password": self.password + "test"}, ) self.assertEqual(response.status_code, 200)
def test_user_delete_post(self): """Test Form render""" 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() response = self.client.post( reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug})) self.assertEqual(response.status_code, 200) self.assertStageRedirects(response, reverse("authentik_core:root-redirect")) self.assertFalse(User.objects.filter(username=self.username).exists())
def test_pending_user(self): """Test with pending user""" plan = FlowPlan(flow_pk=self.flow.pk.hex, stages=[self.stage], 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 self.settings( EMAIL_BACKEND="django.core.mail.backends.locmem.EmailBackend"): response = self.client.post(url) self.assertEqual(response.status_code, 200) self.assertEqual(len(mail.outbox), 1) self.assertEqual(mail.outbox[0].subject, "authentik")
def test_static_hidden_overwrite(self): """Test that static and hidden fields ignore any value sent to them""" plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()]) plan.context[PLAN_CONTEXT_PROMPT] = {"hidden_prompt": "hidden"} self.prompt_data["hidden_prompt"] = "foo" self.prompt_data["static_prompt"] = "foo" challenge_response = PromptChallengeResponse(None, stage=self.stage, plan=plan, data=self.prompt_data) self.assertEqual(challenge_response.is_valid(), True) self.assertNotEqual(challenge_response.validated_data["hidden_prompt"], "foo") self.assertEqual(challenge_response.validated_data["hidden_prompt"], "hidden") self.assertNotEqual(challenge_response.validated_data["static_prompt"], "foo")
def test_valid_password(self): """Test with a valid pending user and backend""" plan = FlowPlan( flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()] ) plan.context[PLAN_CONTEXT_PENDING_USER] = self.user session = self.client.session session[SESSION_KEY_PLAN] = plan session.save() response = self.client.get( reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}) ) self.assertEqual(response.status_code, 200) self.assertJSONEqual( force_str(response.content), {"to": reverse("authentik_core:root-redirect"), "type": "redirect"}, )
def test_valid_post(self): """Test with a valid pending user and backend""" plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()]) plan.context[PLAN_CONTEXT_PENDING_USER] = self.user plan.context[PLAN_CONTEXT_AUTHENTICATION_BACKEND] = BACKEND_INBUILT session = self.client.session session[SESSION_KEY_PLAN] = plan session.save() response = self.client.post( reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug})) # pylint: disable=no-member self.assertEqual(response.status_code, 200) self.assertStageRedirects(response, reverse("authentik_core:root-redirect"))
def test_without_invitation_fail(self): """Test without any invitation, continue_flow_without_invitation not set.""" plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()]) plan.context[PLAN_CONTEXT_PENDING_USER] = self.user plan.context[PLAN_CONTEXT_AUTHENTICATION_BACKEND] = BACKEND_INBUILT session = self.client.session session[SESSION_KEY_PLAN] = plan session.save() response = self.client.get( reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug})) self.assertEqual(response.status_code, 200) self.assertStageResponse( response, flow=self.flow, component="ak-stage-access-denied", )
def test_pending_user(self): """Test with pending user""" 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.post(url) self.assertEqual(response.status_code, 200) self.assertEqual(len(mail.outbox), 1) self.assertEqual(mail.outbox[0].subject, "authentik")