diff --git a/changelog.d/11743.feature b/changelog.d/11743.feature
new file mode 100644
index 0000000000..9809f48b96
--- /dev/null
+++ b/changelog.d/11743.feature
@@ -0,0 +1 @@
+Add a config flag to inhibit M_USER_IN_USE during registration.
diff --git a/docs/sample_config.yaml b/docs/sample_config.yaml
index 1b86d0295d..b38e6d6c88 100644
--- a/docs/sample_config.yaml
+++ b/docs/sample_config.yaml
@@ -1428,6 +1428,16 @@ account_threepid_delegates:
#
#auto_join_rooms_for_guests: false
+# Whether to inhibit errors raised when registering a new account if the user ID
+# already exists. If turned on, that requests to /register/available will always
+# show a user ID as available, and Synapse won't raise an error when starting
+# a registration with a user ID that already exists. However, Synapse will still
+# raise an error if the registration completes and the username conflicts.
+#
+# Defaults to false.
+#
+#inhibit_user_in_use_error: true
+
## Metrics ###
diff --git a/synapse/config/registration.py b/synapse/config/registration.py
index 7a059c6dec..ea9b50fe97 100644
--- a/synapse/config/registration.py
+++ b/synapse/config/registration.py
@@ -190,6 +190,8 @@ class RegistrationConfig(Config):
# The success template used during fallback auth.
self.fallback_success_template = self.read_template("auth_success.html")
+ self.inhibit_user_in_use_error = config.get("inhibit_user_in_use_error", False)
+
def generate_config_section(self, generate_secrets=False, **kwargs):
if generate_secrets:
registration_shared_secret = 'registration_shared_secret: "%s"' % (
@@ -446,6 +448,16 @@ class RegistrationConfig(Config):
# Defaults to true.
#
#auto_join_rooms_for_guests: false
+
+ # Whether to inhibit errors raised when registering a new account if the user ID
+ # already exists. If turned on, that requests to /register/available will always
+ # show a user ID as available, and Synapse won't raise an error when starting
+ # a registration with a user ID that already exists. However, Synapse will still
+ # raise an error if the registration completes and the username conflicts.
+ #
+ # Defaults to false.
+ #
+ #inhibit_user_in_use_error: true
"""
% locals()
)
diff --git a/synapse/handlers/register.py b/synapse/handlers/register.py
index f08a516a75..a719d5eef3 100644
--- a/synapse/handlers/register.py
+++ b/synapse/handlers/register.py
@@ -132,6 +132,7 @@ class RegistrationHandler:
localpart: str,
guest_access_token: Optional[str] = None,
assigned_user_id: Optional[str] = None,
+ inhibit_user_in_use_error: bool = False,
) -> None:
if types.contains_invalid_mxid_characters(localpart):
raise SynapseError(
@@ -171,21 +172,22 @@ class RegistrationHandler:
users = await self.store.get_users_by_id_case_insensitive(user_id)
if users:
- if not guest_access_token:
+ if not inhibit_user_in_use_error and not guest_access_token:
raise SynapseError(
400, "User ID already taken.", errcode=Codes.USER_IN_USE
)
- user_data = await self.auth.get_user_by_access_token(guest_access_token)
- if (
- not user_data.is_guest
- or UserID.from_string(user_data.user_id).localpart != localpart
- ):
- raise AuthError(
- 403,
- "Cannot register taken user ID without valid guest "
- "credentials for that user.",
- errcode=Codes.FORBIDDEN,
- )
+ if guest_access_token:
+ user_data = await self.auth.get_user_by_access_token(guest_access_token)
+ if (
+ not user_data.is_guest
+ or UserID.from_string(user_data.user_id).localpart != localpart
+ ):
+ raise AuthError(
+ 403,
+ "Cannot register taken user ID without valid guest "
+ "credentials for that user.",
+ errcode=Codes.FORBIDDEN,
+ )
if guest_access_token is None:
try:
diff --git a/synapse/rest/client/register.py b/synapse/rest/client/register.py
index 8b56c76aed..c59dae7c03 100644
--- a/synapse/rest/client/register.py
+++ b/synapse/rest/client/register.py
@@ -339,12 +339,19 @@ class UsernameAvailabilityRestServlet(RestServlet):
),
)
+ self.inhibit_user_in_use_error = (
+ hs.config.registration.inhibit_user_in_use_error
+ )
+
async def on_GET(self, request: Request) -> Tuple[int, JsonDict]:
if not self.hs.config.registration.enable_registration:
raise SynapseError(
403, "Registration has been disabled", errcode=Codes.FORBIDDEN
)
+ if self.inhibit_user_in_use_error:
+ return 200, {"available": True}
+
ip = request.getClientIP()
with self.ratelimiter.ratelimit(ip) as wait_deferred:
await wait_deferred
@@ -422,6 +429,9 @@ class RegisterRestServlet(RestServlet):
self._refresh_tokens_enabled = (
hs.config.registration.refreshable_access_token_lifetime is not None
)
+ self._inhibit_user_in_use_error = (
+ hs.config.registration.inhibit_user_in_use_error
+ )
self._registration_flows = _calculate_registration_flows(
hs.config, self.auth_handler
@@ -564,6 +574,7 @@ class RegisterRestServlet(RestServlet):
desired_username,
guest_access_token=guest_access_token,
assigned_user_id=registered_user_id,
+ inhibit_user_in_use_error=self._inhibit_user_in_use_error,
)
# Check if the user-interactive authentication flows are complete, if
diff --git a/tests/rest/client/test_register.py b/tests/rest/client/test_register.py
index 6e7c0f11df..407dd32a73 100644
--- a/tests/rest/client/test_register.py
+++ b/tests/rest/client/test_register.py
@@ -726,6 +726,47 @@ class RegisterRestServletTestCase(unittest.HomeserverTestCase):
{"errcode": "M_UNKNOWN", "error": "Unable to parse email address"},
)
+ @override_config(
+ {
+ "inhibit_user_in_use_error": True,
+ }
+ )
+ def test_inhibit_user_in_use_error(self):
+ """Tests that the 'inhibit_user_in_use_error' configuration flag behaves
+ correctly.
+ """
+ username = "arthur"
+
+ # Manually register the user, so we know the test isn't passing because of a lack
+ # of clashing.
+ reg_handler = self.hs.get_registration_handler()
+ self.get_success(reg_handler.register_user(username))
+
+ # Check that /available correctly ignores the username provided despite the
+ # username being already registered.
+ channel = self.make_request("GET", "register/available?username=" + username)
+ self.assertEquals(200, channel.code, channel.result)
+
+ # Test that when starting a UIA registration flow the request doesn't fail because
+ # of a conflicting username
+ channel = self.make_request(
+ "POST",
+ "register",
+ {"username": username, "type": "m.login.password", "password": "foo"},
+ )
+ self.assertEqual(channel.code, 401)
+ self.assertIn("session", channel.json_body)
+
+ # Test that finishing the registration fails because of a conflicting username.
+ session = channel.json_body["session"]
+ channel = self.make_request(
+ "POST",
+ "register",
+ {"auth": {"session": session, "type": LoginType.DUMMY}},
+ )
+ self.assertEqual(channel.code, 400, channel.json_body)
+ self.assertEqual(channel.json_body["errcode"], Codes.USER_IN_USE)
+
class AccountValidityTestCase(unittest.HomeserverTestCase):
|