diff --git a/authentik/core/models.py b/authentik/core/models.py index c9c0a44df..97080a3b6 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 dfe22de75..51349ec23 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 000000000..61ee52dbd --- /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 dceb4e4cd..9bd1dc6be 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 90ed24b63..f152b9b99 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 80ff2aaae..d985fea08 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 4271d91f2..906df5fa6 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 8c4afaf32..0758c49a6 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 32ef66dfb..0c644ebff 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 5abb27edc..55a40ec4f 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 c5279b4cc..f5f734dd6 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 9c180a948..d102b44f2 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 3ab4acba5..604e316f5 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 d16804f5f..4b036bb0b 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 43be21897..abdd780d1 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 521282fb5..48f5d5aa5 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 f585fbf81..a739af6bd 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 b712a4df4..b0d0c1bc0 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 027dd66de..3647a2556 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 0990a77f1..c0f5a69a4 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.`} +

+