From 529d57fce1d9fd4f92bceeeaa348844f3965d599 Mon Sep 17 00:00:00 2001 From: Anubhav Tandon Date: Fri, 5 Jun 2026 06:38:05 +0530 Subject: [PATCH 1/2] Validating registration before password validation --- api/custom_auth/serializers.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/api/custom_auth/serializers.py b/api/custom_auth/serializers.py index 03cab2cad5fa..9d0ac5887ef7 100644 --- a/api/custom_auth/serializers.py +++ b/api/custom_auth/serializers.py @@ -79,6 +79,11 @@ class Meta(UserCreateSerializer.Meta): # type: ignore[misc] } def validate(self, attrs): # type: ignore[no-untyped-def] + email = attrs.get("email", "") + self._validate_registration_invite( + email=email, sign_up_type=attrs.get("sign_up_type") + ) + attrs = super().validate(attrs) email = attrs.get("email") if attrs.get("superuser"): @@ -101,10 +106,6 @@ def validate(self, attrs): # type: ignore[no-untyped-def] self.context.get("request"), email=email, raise_exception=True ) - self._validate_registration_invite( - email=email, sign_up_type=attrs.get("sign_up_type") - ) - attrs["email"] = email.lower() return attrs From 77a69fd0437e3d268eef2b71cfc4c4166d35a983 Mon Sep 17 00:00:00 2001 From: Anubhav Tandon Date: Mon, 8 Jun 2026 17:47:47 +0530 Subject: [PATCH 2/2] Fix user enumeration in registration by checking invite before email uniqueness --- api/custom_auth/serializers.py | 14 +++++------ .../test_custom_auth_integration.py | 23 +++++++++++++++++++ .../test_unit_custom_auth_serializer.py | 18 +++++++++++++++ 3 files changed, 47 insertions(+), 8 deletions(-) diff --git a/api/custom_auth/serializers.py b/api/custom_auth/serializers.py index 9d0ac5887ef7..f1128bd980c2 100644 --- a/api/custom_auth/serializers.py +++ b/api/custom_auth/serializers.py @@ -6,7 +6,6 @@ from rest_framework import serializers from rest_framework.authtoken.models import Token from rest_framework.exceptions import PermissionDenied -from rest_framework.validators import UniqueValidator from organisations.invites.models import Invite, InviteLink from users.auth_type import AuthType @@ -68,13 +67,7 @@ class Meta(UserCreateSerializer.Meta): # type: ignore[misc] write_only_fields = ("sign_up_type",) extra_kwargs = { "email": { - "validators": [ - UniqueValidator( - queryset=FFAdminUser.objects.all(), - lookup="iexact", - message="Email already exists. Please log in.", - ) - ] + "validators": list[object](), } } @@ -84,6 +77,11 @@ def validate(self, attrs): # type: ignore[no-untyped-def] email=email, sign_up_type=attrs.get("sign_up_type") ) + if FFAdminUser.objects.filter(email__iexact=email).exists(): + raise serializers.ValidationError( + {"email": "Email already exists. Please log in."} + ) + attrs = super().validate(attrs) email = attrs.get("email") if attrs.get("superuser"): diff --git a/api/tests/integration/custom_auth/end_to_end/test_custom_auth_integration.py b/api/tests/integration/custom_auth/end_to_end/test_custom_auth_integration.py index 620025537176..ff36358c4b59 100644 --- a/api/tests/integration/custom_auth/end_to_end/test_custom_auth_integration.py +++ b/api/tests/integration/custom_auth/end_to_end/test_custom_auth_integration.py @@ -122,6 +122,29 @@ def test_register__without_invite_when_disabled__returns_forbidden( assert response.status_code == status.HTTP_403_FORBIDDEN +@override_settings(ALLOW_REGISTRATION_WITHOUT_INVITE=False) # type: ignore[misc] +def test_register__existing_email_without_invite_when_disabled__returns_forbidden( + db: None, + api_client: APIClient, + admin_user: FFAdminUser, +) -> None: + # Given + password = FFAdminUser.objects.make_random_password() + register_data = { + "email": admin_user.email, + "password": password, + "first_name": "test", + "last_name": "register", + } + + # When + url = reverse("api-v1:custom_auth:ffadminuser-list") + response = api_client.post(url, data=register_data) + + # Then + assert response.status_code == status.HTTP_403_FORBIDDEN + + @override_settings(ALLOW_REGISTRATION_WITHOUT_INVITE=False) # type: ignore[misc] def test_register__with_invite_when_registration_disabled__returns_created( db: None, diff --git a/api/tests/unit/custom_auth/test_unit_custom_auth_serializer.py b/api/tests/unit/custom_auth/test_unit_custom_auth_serializer.py index fc2f67c9d227..b4c202da442f 100644 --- a/api/tests/unit/custom_auth/test_unit_custom_auth_serializer.py +++ b/api/tests/unit/custom_auth/test_unit_custom_auth_serializer.py @@ -132,6 +132,24 @@ def test_invite_link_validation__hash_not_provided__raises_permission_denied( assert exc_info.value.detail == USER_REGISTRATION_WITHOUT_INVITE_ERROR_MESSAGE +def test_invite_link_validation__existing_email_without_invite__raises_permission_denied( + db: None, + settings: SettingsWrapper, +) -> None: + # Given + settings.ALLOW_REGISTRATION_WITHOUT_INVITE = False + FFAdminUser.objects.create(email="testuser@mail.com") + + serializer = CustomUserCreateSerializer(data=user_dict) + + # When + with pytest.raises(PermissionDenied) as exc_info: + serializer.is_valid(raise_exception=True) + + # Then + assert exc_info.value.detail == USER_REGISTRATION_WITHOUT_INVITE_ERROR_MESSAGE + + def test_invite_link_validation__invalid_hash__raises_permission_denied( invite_link: InviteLink, settings: SettingsWrapper,