diff --git a/authentik/core/models.py b/authentik/core/models.py index c9c0a44dfb..97080a3b6f 100644 --- a/authentik/core/models.py +++ b/authentik/core/models.py @@ -192,7 +192,7 @@ class User(GuardianUserMixin, AbstractUser): @property def uid(self) -> str: - """Generate a globall unique UID, based on the user ID and the hashed secret key""" + """Generate a globally unique UID, based on the user ID and the hashed secret key""" return sha256(f"{self.id}-{settings.SECRET_KEY}".encode("ascii")).hexdigest() @property diff --git a/authentik/stages/authenticator_sms/api.py b/authentik/stages/authenticator_sms/api.py index dfe22de751..51349ec23f 100644 --- a/authentik/stages/authenticator_sms/api.py +++ b/authentik/stages/authenticator_sms/api.py @@ -26,6 +26,7 @@ class AuthenticatorSMSStageSerializer(StageSerializer): "auth", "auth_password", "auth_type", + "verify_only", ] diff --git a/authentik/stages/authenticator_sms/migrations/0004_authenticatorsmsstage_verify_only_and_more.py b/authentik/stages/authenticator_sms/migrations/0004_authenticatorsmsstage_verify_only_and_more.py new file mode 100644 index 0000000000..61ee52dbd0 --- /dev/null +++ b/authentik/stages/authenticator_sms/migrations/0004_authenticatorsmsstage_verify_only_and_more.py @@ -0,0 +1,25 @@ +# Generated by Django 4.0.4 on 2022-05-24 19:08 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("authentik_stages_authenticator_sms", "0003_smsdevice_last_used_on"), + ] + + operations = [ + migrations.AddField( + model_name="authenticatorsmsstage", + name="verify_only", + field=models.BooleanField( + default=False, + help_text="When enabled, the Phone number is only used during enrollment to verify the users authenticity. Only a hash of the phone number is saved to ensure it is not re-used in the future.", + ), + ), + migrations.AlterUniqueTogether( + name="smsdevice", + unique_together={("stage", "phone_number")}, + ), + ] diff --git a/authentik/stages/authenticator_sms/models.py b/authentik/stages/authenticator_sms/models.py index dceb4e4cd8..9bd1dc6bee 100644 --- a/authentik/stages/authenticator_sms/models.py +++ b/authentik/stages/authenticator_sms/models.py @@ -1,4 +1,5 @@ -"""OTP Time-based models""" +"""SMS Authenticator models""" +from hashlib import sha256 from typing import Optional from django.contrib.auth import get_user_model @@ -46,6 +47,15 @@ class AuthenticatorSMSStage(ConfigurableStage, Stage): auth_password = models.TextField(default="", blank=True) auth_type = models.TextField(choices=SMSAuthTypes.choices, default=SMSAuthTypes.BASIC) + verify_only = models.BooleanField( + default=False, + help_text=_( + "When enabled, the Phone number is only used during enrollment to verify the " + "users authenticity. Only a hash of the phone number is saved to ensure it is " + "not re-used in the future." + ), + ) + def send(self, token: str, device: "SMSDevice"): """Send message via selected provider""" if self.provider == SMSProviders.TWILIO: @@ -158,6 +168,11 @@ class AuthenticatorSMSStage(ConfigurableStage, Stage): verbose_name_plural = _("SMS Authenticator Setup Stages") +def hash_phone_number(phone_number: str) -> str: + """Hash phone number with prefix""" + return "hash:" + sha256(phone_number.encode()).hexdigest() + + class SMSDevice(SideChannelDevice): """SMS Device""" @@ -170,6 +185,15 @@ class SMSDevice(SideChannelDevice): last_t = models.DateTimeField(auto_now=True) + def set_hashed_number(self): + """Set phone_number to hashed number""" + self.phone_number = hash_phone_number(self.phone_number) + + @property + def is_hashed(self) -> bool: + """Check if the phone number is hashed""" + return self.phone_number.startswith("hash:") + def verify_token(self, token): valid = super().verify_token(token) if valid: @@ -182,3 +206,4 @@ class SMSDevice(SideChannelDevice): class Meta: verbose_name = _("SMS Device") verbose_name_plural = _("SMS Devices") + unique_together = (("stage", "phone_number"),) diff --git a/authentik/stages/authenticator_sms/stage.py b/authentik/stages/authenticator_sms/stage.py index 90ed24b63f..f152b9b998 100644 --- a/authentik/stages/authenticator_sms/stage.py +++ b/authentik/stages/authenticator_sms/stage.py @@ -1,6 +1,7 @@ """SMS Setup stage""" from typing import Optional +from django.db.models import Q from django.http import HttpRequest, HttpResponse from django.http.request import QueryDict from django.utils.translation import gettext_lazy as _ @@ -15,7 +16,11 @@ from authentik.flows.challenge import ( ) from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER from authentik.flows.stage import ChallengeStageView -from authentik.stages.authenticator_sms.models import AuthenticatorSMSStage, SMSDevice +from authentik.stages.authenticator_sms.models import ( + AuthenticatorSMSStage, + SMSDevice, + hash_phone_number, +) from authentik.stages.prompt.stage import PLAN_CONTEXT_PROMPT SESSION_KEY_SMS_DEVICE = "authentik/stages/authenticator_sms/sms_device" @@ -45,6 +50,10 @@ class AuthenticatorSMSChallengeResponse(ChallengeResponse): stage: AuthenticatorSMSStage = self.device.stage if "code" not in attrs: self.device.phone_number = attrs["phone_number"] + hashed_number = hash_phone_number(self.device.phone_number) + query = Q(phone_number=hashed_number) | Q(phone_number=self.device.phone_number) + if SMSDevice.objects.filter(query, stage=self.stage.executor.current_stage.pk).exists(): + raise ValidationError(_("Invalid phone number")) # No code yet, but we have a phone number, so send a verification message stage.send(self.device.token, self.device) return super().validate(attrs) @@ -111,6 +120,10 @@ class AuthenticatorSMSStageView(ChallengeStageView): device: SMSDevice = self.request.session[SESSION_KEY_SMS_DEVICE] if not device.confirmed: return self.challenge_invalid(response) + stage: AuthenticatorSMSStage = self.executor.current_stage + if stage.verify_only: + self.logger.debug("Hashing number on device") + device.set_hashed_number() device.save() del self.request.session[SESSION_KEY_SMS_DEVICE] return self.executor.stage_ok() diff --git a/authentik/stages/authenticator_sms/tests.py b/authentik/stages/authenticator_sms/tests.py index 80ff2aaaef..d985fea08b 100644 --- a/authentik/stages/authenticator_sms/tests.py +++ b/authentik/stages/authenticator_sms/tests.py @@ -2,32 +2,31 @@ from unittest.mock import MagicMock, patch from django.urls import reverse -from rest_framework.test import APITestCase -from authentik.core.models import User -from authentik.flows.challenge import ChallengeTypes -from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding -from authentik.stages.authenticator_sms.models import AuthenticatorSMSStage, SMSProviders -from authentik.stages.authenticator_sms.stage import SESSION_KEY_SMS_DEVICE +from authentik.core.tests.utils import create_test_admin_user, create_test_flow +from authentik.flows.models import FlowStageBinding +from authentik.flows.tests import FlowTestCase +from authentik.stages.authenticator_sms.models import ( + AuthenticatorSMSStage, + SMSDevice, + SMSProviders, + hash_phone_number, +) -class AuthenticatorSMSStageTests(APITestCase): +class AuthenticatorSMSStageTests(FlowTestCase): """Test SMS API""" def setUp(self) -> None: super().setUp() - self.flow = Flow.objects.create( - name="foo", - slug="foo", - designation=FlowDesignation.STAGE_CONFIGURATION, - ) - self.stage = AuthenticatorSMSStage.objects.create( + self.flow = create_test_flow() + self.stage: AuthenticatorSMSStage = AuthenticatorSMSStage.objects.create( name="foo", provider=SMSProviders.TWILIO, configure_flow=self.flow, ) FlowStageBinding.objects.create(target=self.flow, stage=self.stage, order=0) - self.user = User.objects.create(username="foo") + self.user = create_test_admin_user() self.client.force_login(self.user) def test_stage_no_prefill(self): @@ -38,27 +37,29 @@ class AuthenticatorSMSStageTests(APITestCase): response = self.client.get( reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}), ) - self.assertJSONEqual( - response.content, - { - "component": "ak-stage-authenticator-sms", - "flow_info": { - "background": self.flow.background_url, - "cancel_url": reverse("authentik_flows:cancel"), - "title": "", - "layout": "stacked", - }, - "pending_user": "foo", - "pending_user_avatar": "/static/dist/assets/images/user_default.png", - "phone_number_required": True, - "type": ChallengeTypes.NATIVE.value, - }, + self.assertStageResponse( + response, + self.flow, + self.user, + component="ak-stage-authenticator-sms", + phone_number_required=True, ) def test_stage_submit(self): """test stage (submit)""" - # Prepares session etc - self.test_stage_no_prefill() + self.client.get( + reverse("authentik_flows:configure", kwargs={"stage_uuid": self.stage.stage_uuid}), + ) + response = self.client.get( + reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}), + ) + self.assertStageResponse( + response, + self.flow, + self.user, + component="ak-stage-authenticator-sms", + phone_number_required=True, + ) sms_send_mock = MagicMock() with patch( "authentik.stages.authenticator_sms.models.AuthenticatorSMSStage.send", @@ -70,23 +71,156 @@ class AuthenticatorSMSStageTests(APITestCase): ) self.assertEqual(response.status_code, 200) sms_send_mock.assert_called_once() + self.assertStageResponse( + response, + self.flow, + self.user, + component="ak-stage-authenticator-sms", + response_errors={}, + phone_number_required=False, + ) def test_stage_submit_full(self): """test stage (submit)""" - # Prepares session etc - self.test_stage_submit() + self.client.get( + reverse("authentik_flows:configure", kwargs={"stage_uuid": self.stage.stage_uuid}), + ) + response = self.client.get( + reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}), + ) + self.assertStageResponse( + response, + self.flow, + self.user, + component="ak-stage-authenticator-sms", + phone_number_required=True, + ) sms_send_mock = MagicMock() with patch( "authentik.stages.authenticator_sms.models.AuthenticatorSMSStage.send", sms_send_mock, + ): + response = self.client.post( + reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}), + data={"component": "ak-stage-authenticator-sms", "phone_number": "foo"}, + ) + self.assertEqual(response.status_code, 200) + sms_send_mock.assert_called_once() + self.assertStageResponse( + response, + self.flow, + self.user, + component="ak-stage-authenticator-sms", + response_errors={}, + phone_number_required=False, + ) + with patch( + "authentik.stages.authenticator_sms.models.SMSDevice.verify_token", + MagicMock(return_value=True), ): response = self.client.post( reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}), data={ "component": "ak-stage-authenticator-sms", "phone_number": "foo", - "code": int(self.client.session[SESSION_KEY_SMS_DEVICE].token), + "code": "123456", }, ) + self.assertEqual(response.status_code, 200) + self.assertStageRedirects(response, reverse("authentik_core:root-redirect")) + + def test_stage_hash(self): + """test stage (verify_only)""" + self.stage.verify_only = True + self.stage.save() + self.client.get( + reverse("authentik_flows:configure", kwargs={"stage_uuid": self.stage.stage_uuid}), + ) + response = self.client.get( + reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}), + ) + self.assertStageResponse( + response, + self.flow, + self.user, + component="ak-stage-authenticator-sms", + phone_number_required=True, + ) + sms_send_mock = MagicMock() + with patch( + "authentik.stages.authenticator_sms.models.AuthenticatorSMSStage.send", + sms_send_mock, + ): + response = self.client.post( + reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}), + data={"component": "ak-stage-authenticator-sms", "phone_number": "foo"}, + ) self.assertEqual(response.status_code, 200) - sms_send_mock.assert_not_called() + sms_send_mock.assert_called_once() + self.assertStageResponse( + response, + self.flow, + self.user, + component="ak-stage-authenticator-sms", + response_errors={}, + phone_number_required=False, + ) + with patch( + "authentik.stages.authenticator_sms.models.SMSDevice.verify_token", + MagicMock(return_value=True), + ): + response = self.client.post( + reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}), + data={ + "component": "ak-stage-authenticator-sms", + "phone_number": "foo", + "code": "123456", + }, + ) + self.assertEqual(response.status_code, 200) + self.assertStageRedirects(response, reverse("authentik_core:root-redirect")) + device: SMSDevice = SMSDevice.objects.filter(user=self.user).first() + self.assertTrue(device.is_hashed) + + def test_stage_hash_twice(self): + """test stage (hash + duplicate)""" + SMSDevice.objects.create( + user=create_test_admin_user(), + stage=self.stage, + phone_number=hash_phone_number("foo"), + ) + self.stage.verify_only = True + self.stage.save() + self.client.get( + reverse("authentik_flows:configure", kwargs={"stage_uuid": self.stage.stage_uuid}), + ) + response = self.client.get( + reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}), + ) + self.assertStageResponse( + response, + self.flow, + self.user, + component="ak-stage-authenticator-sms", + phone_number_required=True, + ) + sms_send_mock = MagicMock() + with patch( + "authentik.stages.authenticator_sms.models.AuthenticatorSMSStage.send", + sms_send_mock, + ): + response = self.client.post( + reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}), + data={"component": "ak-stage-authenticator-sms", "phone_number": "foo"}, + ) + self.assertEqual(response.status_code, 200) + self.assertStageResponse( + response, + self.flow, + self.user, + component="ak-stage-authenticator-sms", + response_errors={ + "non_field_errors": [{"code": "invalid", "string": "Invalid phone number"}] + }, + phone_number_required=False, + ) diff --git a/authentik/stages/authenticator_validate/stage.py b/authentik/stages/authenticator_validate/stage.py index 4271d91f21..906df5fa6f 100644 --- a/authentik/stages/authenticator_validate/stage.py +++ b/authentik/stages/authenticator_validate/stage.py @@ -168,6 +168,9 @@ class AuthenticatorValidateStageView(ChallengeStageView): if device_class not in stage.device_classes: self.logger.debug("device class not allowed", device_class=device_class) continue + if isinstance(device, SMSDevice) and device.is_hashed: + LOGGER.debug("Hashed SMS device, skipping") + continue allowed_devices.append(device) # Ensure only one challenge per device class # WebAuthn does another device loop to find all WebAuthn devices diff --git a/authentik/stages/authenticator_validate/tests/test_sms.py b/authentik/stages/authenticator_validate/tests/test_sms.py index 8c4afaf32c..0758c49a67 100644 --- a/authentik/stages/authenticator_validate/tests/test_sms.py +++ b/authentik/stages/authenticator_validate/tests/test_sms.py @@ -116,3 +116,37 @@ class AuthenticatorValidateStageSMSTests(FlowTestCase): ) self.assertIn(COOKIE_NAME_MFA, response.cookies) self.assertStageResponse(response, component="xak-flow-redirect", to="/") + + def test_sms_hashed(self): + """Test hashed SMS device""" + ident_stage = IdentificationStage.objects.create( + name="conf", + user_fields=[ + UserFields.USERNAME, + ], + ) + SMSDevice.objects.create( + user=self.user, + confirmed=True, + stage=self.stage, + phone_number="hash:foo", + ) + + stage = AuthenticatorValidateStage.objects.create( + name="foo", + last_auth_threshold="hours=1", + not_configured_action=NotConfiguredAction.DENY, + device_classes=[DeviceClasses.SMS], + ) + stage.configuration_stages.set([ident_stage]) + flow = Flow.objects.create(name="test", slug="test", title="test") + FlowStageBinding.objects.create(target=flow, stage=ident_stage, order=0) + FlowStageBinding.objects.create(target=flow, stage=stage, order=1) + + response = self.client.post( + reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}), + {"uid_field": self.user.username}, + follow=True, + ) + self.assertEqual(response.status_code, 200) + self.assertStageResponse(response, flow, self.user, component="ak-stage-access-denied") diff --git a/schema.yml b/schema.yml index 32ef66dfb9..0c644ebff5 100644 --- a/schema.yml +++ b/schema.yml @@ -14436,6 +14436,10 @@ paths: schema: type: string format: uuid + - in: query + name: verify_only + schema: + type: boolean tags: - stages security: @@ -19608,6 +19612,11 @@ components: type: string auth_type: $ref: '#/components/schemas/AuthTypeEnum' + verify_only: + type: boolean + description: When enabled, the Phone number is only used during enrollment + to verify the users authenticity. Only a hash of the phone number is saved + to ensure it is not re-used in the future. required: - account_sid - auth @@ -19651,6 +19660,11 @@ components: type: string auth_type: $ref: '#/components/schemas/AuthTypeEnum' + verify_only: + type: boolean + description: When enabled, the Phone number is only used during enrollment + to verify the users authenticity. Only a hash of the phone number is saved + to ensure it is not re-used in the future. required: - account_sid - auth @@ -23042,7 +23056,6 @@ components: description: Only send notification once, for example when sending a webhook into a chat channel. required: - - mode - mode_verbose - name - pk @@ -23074,7 +23087,6 @@ components: description: Only send notification once, for example when sending a webhook into a chat channel. required: - - mode - name NotificationTransportTest: type: object @@ -26804,6 +26816,11 @@ components: type: string auth_type: $ref: '#/components/schemas/AuthTypeEnum' + verify_only: + type: boolean + description: When enabled, the Phone number is only used during enrollment + to verify the users authenticity. Only a hash of the phone number is saved + to ensure it is not re-used in the future. PatchedAuthenticatorStaticStageRequest: type: object description: AuthenticatorStaticStage Serializer diff --git a/web/src/locales/de.po b/web/src/locales/de.po index 5abb27edc9..55a40ec4ff 100644 --- a/web/src/locales/de.po +++ b/web/src/locales/de.po @@ -3510,6 +3510,7 @@ msgstr "Benachrichtigungen" msgid "Number" msgstr "Nummer" +#: src/pages/stages/authenticator_sms/AuthenticatorSMSStageForm.ts #: src/pages/stages/authenticator_sms/AuthenticatorSMSStageForm.ts msgid "Number the SMS will be sent from." msgstr "Nummer, von der die SMS gesendet wird" @@ -6277,6 +6278,10 @@ msgstr "Zertifikat zur Überprüfung" msgid "Verification certificates" msgstr "" +#: src/pages/stages/authenticator_sms/AuthenticatorSMSStageForm.ts +msgid "Verify only" +msgstr "" + #: src/pages/stages/email/EmailStageForm.ts msgid "Verify the user's email address by sending them a one-time-link. Can also be used for recovery to verify the user's authenticity." msgstr "Überprüfen Sie die E-Mail-Adresse des Benutzers, indem Sie ihm einen einmaligen Link senden. Kann auch für die Wiederherstellung verwendet werden, um die Authentizität des Benutzers zu überprüfen." diff --git a/web/src/locales/en.po b/web/src/locales/en.po index c5279b4ccf..f5f734dd6a 100644 --- a/web/src/locales/en.po +++ b/web/src/locales/en.po @@ -3568,6 +3568,7 @@ msgstr "Notifications" msgid "Number" msgstr "Number" +#: src/pages/stages/authenticator_sms/AuthenticatorSMSStageForm.ts #: src/pages/stages/authenticator_sms/AuthenticatorSMSStageForm.ts msgid "Number the SMS will be sent from." msgstr "Number the SMS will be sent from." @@ -6403,6 +6404,10 @@ msgstr "Verification Certificate" msgid "Verification certificates" msgstr "Verification certificates" +#: src/pages/stages/authenticator_sms/AuthenticatorSMSStageForm.ts +msgid "Verify only" +msgstr "Verify only" + #: src/pages/stages/email/EmailStageForm.ts msgid "Verify the user's email address by sending them a one-time-link. Can also be used for recovery to verify the user's authenticity." msgstr "Verify the user's email address by sending them a one-time-link. Can also be used for recovery to verify the user's authenticity." diff --git a/web/src/locales/es.po b/web/src/locales/es.po index 9c180a948a..d102b44f21 100644 --- a/web/src/locales/es.po +++ b/web/src/locales/es.po @@ -3503,6 +3503,7 @@ msgstr "Notificaciones" msgid "Number" msgstr "Número" +#: src/pages/stages/authenticator_sms/AuthenticatorSMSStageForm.ts #: src/pages/stages/authenticator_sms/AuthenticatorSMSStageForm.ts msgid "Number the SMS will be sent from." msgstr "Número desde el que se enviará el SMS." @@ -6271,6 +6272,10 @@ msgstr "Certificado de verificación" msgid "Verification certificates" msgstr "" +#: src/pages/stages/authenticator_sms/AuthenticatorSMSStageForm.ts +msgid "Verify only" +msgstr "" + #: src/pages/stages/email/EmailStageForm.ts msgid "Verify the user's email address by sending them a one-time-link. Can also be used for recovery to verify the user's authenticity." msgstr "Verifique la dirección de correo electrónico del usuario enviándole un enlace único. También se puede utilizar para la recuperación para verificar la autenticidad del usuario." diff --git a/web/src/locales/fr_FR.po b/web/src/locales/fr_FR.po index 3ab4acba52..604e316f50 100644 --- a/web/src/locales/fr_FR.po +++ b/web/src/locales/fr_FR.po @@ -3537,6 +3537,7 @@ msgstr "Notifications" msgid "Number" msgstr "Nombre" +#: src/pages/stages/authenticator_sms/AuthenticatorSMSStageForm.ts #: src/pages/stages/authenticator_sms/AuthenticatorSMSStageForm.ts msgid "Number the SMS will be sent from." msgstr "" @@ -6332,6 +6333,10 @@ msgstr "Certificat de validation" msgid "Verification certificates" msgstr "" +#: src/pages/stages/authenticator_sms/AuthenticatorSMSStageForm.ts +msgid "Verify only" +msgstr "" + #: src/pages/stages/email/EmailStageForm.ts msgid "Verify the user's email address by sending them a one-time-link. Can also be used for recovery to verify the user's authenticity." msgstr "Vérifier le courriel de l'utilisateur en lui envoyant un lien à usage unique. Peut également être utilisé lors de la récupération afin de vérifier l'authenticité de l'utilisateur." diff --git a/web/src/locales/pl.po b/web/src/locales/pl.po index d16804f5f2..4b036bb0bb 100644 --- a/web/src/locales/pl.po +++ b/web/src/locales/pl.po @@ -3500,6 +3500,7 @@ msgstr "Powiadomienia" msgid "Number" msgstr "Numer" +#: src/pages/stages/authenticator_sms/AuthenticatorSMSStageForm.ts #: src/pages/stages/authenticator_sms/AuthenticatorSMSStageForm.ts msgid "Number the SMS will be sent from." msgstr "Numer, z którego zostanie wysłana wiadomość SMS." @@ -6268,6 +6269,10 @@ msgstr "Certyfikat weryfikacji" msgid "Verification certificates" msgstr "" +#: src/pages/stages/authenticator_sms/AuthenticatorSMSStageForm.ts +msgid "Verify only" +msgstr "" + #: src/pages/stages/email/EmailStageForm.ts msgid "Verify the user's email address by sending them a one-time-link. Can also be used for recovery to verify the user's authenticity." msgstr "Zweryfikuj adres e-mail użytkownika, wysyłając mu jednorazowy link. Może być również używany do odzyskiwania w celu weryfikacji autentyczności użytkownika." diff --git a/web/src/locales/pseudo-LOCALE.po b/web/src/locales/pseudo-LOCALE.po index 43be218979..abdd780d1b 100644 --- a/web/src/locales/pseudo-LOCALE.po +++ b/web/src/locales/pseudo-LOCALE.po @@ -3550,6 +3550,7 @@ msgstr "" msgid "Number" msgstr "" +#: src/pages/stages/authenticator_sms/AuthenticatorSMSStageForm.ts #: src/pages/stages/authenticator_sms/AuthenticatorSMSStageForm.ts msgid "Number the SMS will be sent from." msgstr "" @@ -6373,6 +6374,10 @@ msgstr "" msgid "Verification certificates" msgstr "" +#: src/pages/stages/authenticator_sms/AuthenticatorSMSStageForm.ts +msgid "Verify only" +msgstr "" + #: src/pages/stages/email/EmailStageForm.ts msgid "Verify the user's email address by sending them a one-time-link. Can also be used for recovery to verify the user's authenticity." msgstr "" diff --git a/web/src/locales/tr.po b/web/src/locales/tr.po index 521282fb59..48f5d5aa58 100644 --- a/web/src/locales/tr.po +++ b/web/src/locales/tr.po @@ -3505,6 +3505,7 @@ msgstr "Bildirimler" msgid "Number" msgstr "Numara" +#: src/pages/stages/authenticator_sms/AuthenticatorSMSStageForm.ts #: src/pages/stages/authenticator_sms/AuthenticatorSMSStageForm.ts msgid "Number the SMS will be sent from." msgstr "Numara SMS gönderilecektir." @@ -6273,6 +6274,10 @@ msgstr "Doğrulama Sertifikası" msgid "Verification certificates" msgstr "" +#: src/pages/stages/authenticator_sms/AuthenticatorSMSStageForm.ts +msgid "Verify only" +msgstr "" + #: src/pages/stages/email/EmailStageForm.ts msgid "Verify the user's email address by sending them a one-time-link. Can also be used for recovery to verify the user's authenticity." msgstr "Kullanıcının e-posta adresini bir kerelik bağlantı göndererek doğrulayın. Kullanıcının orijinalliğini doğrulamak için kurtarma için de kullanılabilir." diff --git a/web/src/locales/zh-Hans.po b/web/src/locales/zh-Hans.po index f585fbf81d..a739af6bdf 100644 --- a/web/src/locales/zh-Hans.po +++ b/web/src/locales/zh-Hans.po @@ -3485,6 +3485,7 @@ msgstr "通知" msgid "Number" msgstr "数字" +#: src/pages/stages/authenticator_sms/AuthenticatorSMSStageForm.ts #: src/pages/stages/authenticator_sms/AuthenticatorSMSStageForm.ts msgid "Number the SMS will be sent from." msgstr "短信的发信人号码。" @@ -6230,6 +6231,10 @@ msgstr "验证证书" msgid "Verification certificates" msgstr "验证证书" +#: src/pages/stages/authenticator_sms/AuthenticatorSMSStageForm.ts +msgid "Verify only" +msgstr "" + #: src/pages/stages/email/EmailStageForm.ts msgid "Verify the user's email address by sending them a one-time-link. Can also be used for recovery to verify the user's authenticity." msgstr "通过向用户发送一次性链接来验证用户的电子邮件地址。也可用于在恢复时验证用户的真实性。" diff --git a/web/src/locales/zh-Hant.po b/web/src/locales/zh-Hant.po index b712a4df4c..b0d0c1bc05 100644 --- a/web/src/locales/zh-Hant.po +++ b/web/src/locales/zh-Hant.po @@ -3489,6 +3489,7 @@ msgstr "通知" msgid "Number" msgstr "编号" +#: src/pages/stages/authenticator_sms/AuthenticatorSMSStageForm.ts #: src/pages/stages/authenticator_sms/AuthenticatorSMSStageForm.ts msgid "Number the SMS will be sent from." msgstr "发送短信的来源号码。" @@ -6239,6 +6240,10 @@ msgstr "验证证书" msgid "Verification certificates" msgstr "验证证书" +#: src/pages/stages/authenticator_sms/AuthenticatorSMSStageForm.ts +msgid "Verify only" +msgstr "" + #: src/pages/stages/email/EmailStageForm.ts msgid "Verify the user's email address by sending them a one-time-link. Can also be used for recovery to verify the user's authenticity." msgstr "通过向用户发送一次性链接来验证用户的电子邮件地址。也可用于恢复,以验证用户的真实性。" diff --git a/web/src/locales/zh_TW.po b/web/src/locales/zh_TW.po index 027dd66de1..3647a2556e 100644 --- a/web/src/locales/zh_TW.po +++ b/web/src/locales/zh_TW.po @@ -3489,6 +3489,7 @@ msgstr "通知" msgid "Number" msgstr "编号" +#: src/pages/stages/authenticator_sms/AuthenticatorSMSStageForm.ts #: src/pages/stages/authenticator_sms/AuthenticatorSMSStageForm.ts msgid "Number the SMS will be sent from." msgstr "发送短信的来源号码。" @@ -6239,6 +6240,10 @@ msgstr "验证证书" msgid "Verification certificates" msgstr "验证证书" +#: src/pages/stages/authenticator_sms/AuthenticatorSMSStageForm.ts +msgid "Verify only" +msgstr "" + #: src/pages/stages/email/EmailStageForm.ts msgid "Verify the user's email address by sending them a one-time-link. Can also be used for recovery to verify the user's authenticity." msgstr "通过向用户发送一次性链接来验证用户的电子邮件地址。也可用于恢复,以验证用户的真实性。" diff --git a/web/src/pages/stages/authenticator_sms/AuthenticatorSMSStageForm.ts b/web/src/pages/stages/authenticator_sms/AuthenticatorSMSStageForm.ts index 0990a77f19..c0f5a69a45 100644 --- a/web/src/pages/stages/authenticator_sms/AuthenticatorSMSStageForm.ts +++ b/web/src/pages/stages/authenticator_sms/AuthenticatorSMSStageForm.ts @@ -18,6 +18,7 @@ import { DEFAULT_CONFIG } from "../../../api/Config"; import "../../../elements/forms/FormGroup"; import "../../../elements/forms/HorizontalFormElement"; import { ModelForm } from "../../../elements/forms/ModelForm"; +import { first } from "../../../utils"; @customElement("ak-stage-authenticator-sms-form") export class AuthenticatorSMSStageForm extends ModelForm { @@ -215,6 +216,19 @@ export class AuthenticatorSMSStageForm extends ModelForm +
+ + +
+

+ ${t`If enabled, only a hash of the phone number will be saved. This can be done for data-protection reasons.Devices created from a stage with this enabled cannot be used with the authenticator validation stage.`} +

+