authentik.stages.authenticator_validate.tests.test_email
Test validator stage for Email devices
1"""Test validator stage for Email devices""" 2 3from django.test.client import RequestFactory 4from django.urls.base import reverse 5 6from authentik.core.tests.utils import create_test_admin_user, create_test_flow 7from authentik.flows.models import FlowStageBinding, NotConfiguredAction 8from authentik.flows.tests import FlowTestCase 9from authentik.lib.generators import generate_id 10from authentik.lib.utils.email import mask_email 11from authentik.stages.authenticator_email.models import AuthenticatorEmailStage, EmailDevice 12from authentik.stages.authenticator_validate.models import AuthenticatorValidateStage, DeviceClasses 13from authentik.stages.identification.models import IdentificationStage, UserFields 14 15 16class AuthenticatorValidateStageEmailTests(FlowTestCase): 17 """Test validator stage for Email devices""" 18 19 def setUp(self) -> None: 20 self.user = create_test_admin_user() 21 self.request_factory = RequestFactory() 22 # Create email authenticator stage 23 self.stage = AuthenticatorEmailStage.objects.create( 24 name="email-authenticator", 25 use_global_settings=True, 26 from_address="test@authentik.local", 27 ) 28 # Create identification stage 29 self.ident_stage = IdentificationStage.objects.create( 30 name=generate_id(), 31 user_fields=[UserFields.USERNAME], 32 ) 33 # Create validation stage 34 self.validate_stage = AuthenticatorValidateStage.objects.create( 35 name=generate_id(), 36 device_classes=[DeviceClasses.EMAIL], 37 ) 38 # Create flow with both stages 39 self.flow = create_test_flow() 40 FlowStageBinding.objects.create(target=self.flow, stage=self.ident_stage, order=0) 41 FlowStageBinding.objects.create(target=self.flow, stage=self.validate_stage, order=1) 42 43 def _identify_user(self): 44 """Helper to identify user in flow""" 45 response = self.client.post( 46 reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}), 47 {"uid_field": self.user.username}, 48 follow=True, 49 ) 50 self.assertEqual(response.status_code, 200) 51 return response 52 53 def _send_challenge(self, device): 54 """Helper to send challenge for device""" 55 response = self.client.post( 56 reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}), 57 { 58 "component": "ak-stage-authenticator-validate", 59 "selected_challenge": { 60 "device_class": "email", 61 "device_uid": str(device.pk), 62 "challenge": {}, 63 "last_used": device.last_used.isoformat() if device.last_used else None, 64 }, 65 }, 66 ) 67 self.assertEqual(response.status_code, 200) 68 return response 69 70 def test_happy_path(self): 71 """Test validator stage with valid code""" 72 # Create a device for our user 73 device = EmailDevice.objects.create( 74 user=self.user, 75 confirmed=True, 76 stage=self.stage, 77 email="xx@0.co", 78 ) # Short email for testing purposes 79 80 # First identify the user 81 self._identify_user() 82 83 # Send the challenge 84 response = self._send_challenge(device) 85 response_data = self.assertStageResponse( 86 response, 87 flow=self.flow, 88 component="ak-stage-authenticator-validate", 89 ) 90 91 # Get the device challenge from the response and verify it matches 92 device_challenge = response_data["device_challenges"][0] 93 self.assertEqual(device_challenge["device_class"], "email") 94 self.assertEqual(device_challenge["device_uid"], str(device.pk)) 95 self.assertEqual(device_challenge["challenge"], {"email": mask_email(device.email)}) 96 97 # Generate a token for the device 98 device.generate_token() 99 100 # Submit the valid code 101 response = self.client.post( 102 reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}), 103 {"component": "ak-stage-authenticator-validate", "code": device.token}, 104 ) 105 # Should redirect to root since this is the last stage 106 self.assertStageRedirects(response, "/") 107 108 def test_no_device(self): 109 """Test validator stage without configured device""" 110 configuration_stage = AuthenticatorEmailStage.objects.create( 111 name=generate_id(), 112 use_global_settings=True, 113 from_address="test@authentik.local", 114 ) 115 stage = AuthenticatorValidateStage.objects.create( 116 name=generate_id(), 117 not_configured_action=NotConfiguredAction.CONFIGURE, 118 device_classes=[DeviceClasses.EMAIL], 119 ) 120 stage.configuration_stages.set([configuration_stage]) 121 flow = create_test_flow() 122 FlowStageBinding.objects.create(target=flow, stage=stage, order=2) 123 124 response = self.client.post( 125 reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}), 126 {"component": "ak-stage-authenticator-validate"}, 127 ) 128 self.assertEqual(response.status_code, 200) 129 response_data = self.assertStageResponse( 130 response, 131 flow=flow, 132 component="ak-stage-authenticator-validate", 133 ) 134 self.assertEqual(response_data["configuration_stages"], []) 135 self.assertEqual(response_data["device_challenges"], []) 136 self.assertEqual( 137 response_data["response_errors"], 138 {"non_field_errors": [{"code": "invalid", "string": "Empty response"}]}, 139 ) 140 141 def test_invalid_code(self): 142 """Test validator stage with invalid code""" 143 # Create a device for our user 144 device = EmailDevice.objects.create( 145 user=self.user, 146 confirmed=True, 147 stage=self.stage, 148 email="test@authentik.local", 149 ) 150 151 # First identify the user 152 self._identify_user() 153 154 # Send the challenge 155 self._send_challenge(device) 156 157 # Generate a token for the device 158 device.generate_token() 159 160 # Try invalid code and verify error message 161 response = self.client.post( 162 reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}), 163 {"component": "ak-stage-authenticator-validate", "code": "invalid"}, 164 ) 165 response_data = self.assertStageResponse( 166 response, 167 flow=self.flow, 168 component="ak-stage-authenticator-validate", 169 ) 170 self.assertEqual( 171 response_data["response_errors"], 172 { 173 "code": [ 174 { 175 "code": "invalid", 176 "string": ( 177 "Invalid Token. Please ensure the time on your device " 178 "is accurate and try again." 179 ), 180 } 181 ], 182 }, 183 )
17class AuthenticatorValidateStageEmailTests(FlowTestCase): 18 """Test validator stage for Email devices""" 19 20 def setUp(self) -> None: 21 self.user = create_test_admin_user() 22 self.request_factory = RequestFactory() 23 # Create email authenticator stage 24 self.stage = AuthenticatorEmailStage.objects.create( 25 name="email-authenticator", 26 use_global_settings=True, 27 from_address="test@authentik.local", 28 ) 29 # Create identification stage 30 self.ident_stage = IdentificationStage.objects.create( 31 name=generate_id(), 32 user_fields=[UserFields.USERNAME], 33 ) 34 # Create validation stage 35 self.validate_stage = AuthenticatorValidateStage.objects.create( 36 name=generate_id(), 37 device_classes=[DeviceClasses.EMAIL], 38 ) 39 # Create flow with both stages 40 self.flow = create_test_flow() 41 FlowStageBinding.objects.create(target=self.flow, stage=self.ident_stage, order=0) 42 FlowStageBinding.objects.create(target=self.flow, stage=self.validate_stage, order=1) 43 44 def _identify_user(self): 45 """Helper to identify user in flow""" 46 response = self.client.post( 47 reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}), 48 {"uid_field": self.user.username}, 49 follow=True, 50 ) 51 self.assertEqual(response.status_code, 200) 52 return response 53 54 def _send_challenge(self, device): 55 """Helper to send challenge for device""" 56 response = self.client.post( 57 reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}), 58 { 59 "component": "ak-stage-authenticator-validate", 60 "selected_challenge": { 61 "device_class": "email", 62 "device_uid": str(device.pk), 63 "challenge": {}, 64 "last_used": device.last_used.isoformat() if device.last_used else None, 65 }, 66 }, 67 ) 68 self.assertEqual(response.status_code, 200) 69 return response 70 71 def test_happy_path(self): 72 """Test validator stage with valid code""" 73 # Create a device for our user 74 device = EmailDevice.objects.create( 75 user=self.user, 76 confirmed=True, 77 stage=self.stage, 78 email="xx@0.co", 79 ) # Short email for testing purposes 80 81 # First identify the user 82 self._identify_user() 83 84 # Send the challenge 85 response = self._send_challenge(device) 86 response_data = self.assertStageResponse( 87 response, 88 flow=self.flow, 89 component="ak-stage-authenticator-validate", 90 ) 91 92 # Get the device challenge from the response and verify it matches 93 device_challenge = response_data["device_challenges"][0] 94 self.assertEqual(device_challenge["device_class"], "email") 95 self.assertEqual(device_challenge["device_uid"], str(device.pk)) 96 self.assertEqual(device_challenge["challenge"], {"email": mask_email(device.email)}) 97 98 # Generate a token for the device 99 device.generate_token() 100 101 # Submit the valid code 102 response = self.client.post( 103 reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}), 104 {"component": "ak-stage-authenticator-validate", "code": device.token}, 105 ) 106 # Should redirect to root since this is the last stage 107 self.assertStageRedirects(response, "/") 108 109 def test_no_device(self): 110 """Test validator stage without configured device""" 111 configuration_stage = AuthenticatorEmailStage.objects.create( 112 name=generate_id(), 113 use_global_settings=True, 114 from_address="test@authentik.local", 115 ) 116 stage = AuthenticatorValidateStage.objects.create( 117 name=generate_id(), 118 not_configured_action=NotConfiguredAction.CONFIGURE, 119 device_classes=[DeviceClasses.EMAIL], 120 ) 121 stage.configuration_stages.set([configuration_stage]) 122 flow = create_test_flow() 123 FlowStageBinding.objects.create(target=flow, stage=stage, order=2) 124 125 response = self.client.post( 126 reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}), 127 {"component": "ak-stage-authenticator-validate"}, 128 ) 129 self.assertEqual(response.status_code, 200) 130 response_data = self.assertStageResponse( 131 response, 132 flow=flow, 133 component="ak-stage-authenticator-validate", 134 ) 135 self.assertEqual(response_data["configuration_stages"], []) 136 self.assertEqual(response_data["device_challenges"], []) 137 self.assertEqual( 138 response_data["response_errors"], 139 {"non_field_errors": [{"code": "invalid", "string": "Empty response"}]}, 140 ) 141 142 def test_invalid_code(self): 143 """Test validator stage with invalid code""" 144 # Create a device for our user 145 device = EmailDevice.objects.create( 146 user=self.user, 147 confirmed=True, 148 stage=self.stage, 149 email="test@authentik.local", 150 ) 151 152 # First identify the user 153 self._identify_user() 154 155 # Send the challenge 156 self._send_challenge(device) 157 158 # Generate a token for the device 159 device.generate_token() 160 161 # Try invalid code and verify error message 162 response = self.client.post( 163 reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}), 164 {"component": "ak-stage-authenticator-validate", "code": "invalid"}, 165 ) 166 response_data = self.assertStageResponse( 167 response, 168 flow=self.flow, 169 component="ak-stage-authenticator-validate", 170 ) 171 self.assertEqual( 172 response_data["response_errors"], 173 { 174 "code": [ 175 { 176 "code": "invalid", 177 "string": ( 178 "Invalid Token. Please ensure the time on your device " 179 "is accurate and try again." 180 ), 181 } 182 ], 183 }, 184 )
Test validator stage for Email devices
def
setUp(self) -> None:
20 def setUp(self) -> None: 21 self.user = create_test_admin_user() 22 self.request_factory = RequestFactory() 23 # Create email authenticator stage 24 self.stage = AuthenticatorEmailStage.objects.create( 25 name="email-authenticator", 26 use_global_settings=True, 27 from_address="test@authentik.local", 28 ) 29 # Create identification stage 30 self.ident_stage = IdentificationStage.objects.create( 31 name=generate_id(), 32 user_fields=[UserFields.USERNAME], 33 ) 34 # Create validation stage 35 self.validate_stage = AuthenticatorValidateStage.objects.create( 36 name=generate_id(), 37 device_classes=[DeviceClasses.EMAIL], 38 ) 39 # Create flow with both stages 40 self.flow = create_test_flow() 41 FlowStageBinding.objects.create(target=self.flow, stage=self.ident_stage, order=0) 42 FlowStageBinding.objects.create(target=self.flow, stage=self.validate_stage, order=1)
Hook method for setting up the test fixture before exercising it.
def
test_happy_path(self):
71 def test_happy_path(self): 72 """Test validator stage with valid code""" 73 # Create a device for our user 74 device = EmailDevice.objects.create( 75 user=self.user, 76 confirmed=True, 77 stage=self.stage, 78 email="xx@0.co", 79 ) # Short email for testing purposes 80 81 # First identify the user 82 self._identify_user() 83 84 # Send the challenge 85 response = self._send_challenge(device) 86 response_data = self.assertStageResponse( 87 response, 88 flow=self.flow, 89 component="ak-stage-authenticator-validate", 90 ) 91 92 # Get the device challenge from the response and verify it matches 93 device_challenge = response_data["device_challenges"][0] 94 self.assertEqual(device_challenge["device_class"], "email") 95 self.assertEqual(device_challenge["device_uid"], str(device.pk)) 96 self.assertEqual(device_challenge["challenge"], {"email": mask_email(device.email)}) 97 98 # Generate a token for the device 99 device.generate_token() 100 101 # Submit the valid code 102 response = self.client.post( 103 reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}), 104 {"component": "ak-stage-authenticator-validate", "code": device.token}, 105 ) 106 # Should redirect to root since this is the last stage 107 self.assertStageRedirects(response, "/")
Test validator stage with valid code
def
test_no_device(self):
109 def test_no_device(self): 110 """Test validator stage without configured device""" 111 configuration_stage = AuthenticatorEmailStage.objects.create( 112 name=generate_id(), 113 use_global_settings=True, 114 from_address="test@authentik.local", 115 ) 116 stage = AuthenticatorValidateStage.objects.create( 117 name=generate_id(), 118 not_configured_action=NotConfiguredAction.CONFIGURE, 119 device_classes=[DeviceClasses.EMAIL], 120 ) 121 stage.configuration_stages.set([configuration_stage]) 122 flow = create_test_flow() 123 FlowStageBinding.objects.create(target=flow, stage=stage, order=2) 124 125 response = self.client.post( 126 reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}), 127 {"component": "ak-stage-authenticator-validate"}, 128 ) 129 self.assertEqual(response.status_code, 200) 130 response_data = self.assertStageResponse( 131 response, 132 flow=flow, 133 component="ak-stage-authenticator-validate", 134 ) 135 self.assertEqual(response_data["configuration_stages"], []) 136 self.assertEqual(response_data["device_challenges"], []) 137 self.assertEqual( 138 response_data["response_errors"], 139 {"non_field_errors": [{"code": "invalid", "string": "Empty response"}]}, 140 )
Test validator stage without configured device
def
test_invalid_code(self):
142 def test_invalid_code(self): 143 """Test validator stage with invalid code""" 144 # Create a device for our user 145 device = EmailDevice.objects.create( 146 user=self.user, 147 confirmed=True, 148 stage=self.stage, 149 email="test@authentik.local", 150 ) 151 152 # First identify the user 153 self._identify_user() 154 155 # Send the challenge 156 self._send_challenge(device) 157 158 # Generate a token for the device 159 device.generate_token() 160 161 # Try invalid code and verify error message 162 response = self.client.post( 163 reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}), 164 {"component": "ak-stage-authenticator-validate", "code": "invalid"}, 165 ) 166 response_data = self.assertStageResponse( 167 response, 168 flow=self.flow, 169 component="ak-stage-authenticator-validate", 170 ) 171 self.assertEqual( 172 response_data["response_errors"], 173 { 174 "code": [ 175 { 176 "code": "invalid", 177 "string": ( 178 "Invalid Token. Please ensure the time on your device " 179 "is accurate and try again." 180 ), 181 } 182 ], 183 }, 184 )
Test validator stage with invalid code