Skip to content
This repository was archived by the owner on Apr 26, 2024. It is now read-only.

Commit 95b3f95

Browse files
authored
Add a config flag to inhibit M_USER_IN_USE during registration (#11743)
This is mostly motivated by the tchap use case, where usernames are automatically generated from the user's email address (in a way that allows figuring out the email address from the username). Therefore, it's an issue if we respond to requests on /register and /register/available with M_USER_IN_USE, because it can potentially leak email addresses (which include the user's real name and place of work). This commit adds a flag to inhibit the M_USER_IN_USE errors that are raised both by /register/available, and when providing a username early into the registration process. This error will still be raised if the user completes the registration process but the username conflicts. This is particularly useful when using modules (#11790 adds a module callback to set the username of users at registration) or SSO, since they can ensure the username is unique. More context is available in the PR that introduced this behaviour to synapse-dinsic: matrix-org/synapse-dinsic#48 - as well as the issue in the matrix-dinsic repo: matrix-org/matrix-dinsic#476
1 parent 74e4419 commit 95b3f95

File tree

6 files changed

+89
-12
lines changed

6 files changed

+89
-12
lines changed

changelog.d/11743.feature

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add a config flag to inhibit M_USER_IN_USE during registration.

docs/sample_config.yaml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1428,6 +1428,16 @@ account_threepid_delegates:
14281428
#
14291429
#auto_join_rooms_for_guests: false
14301430

1431+
# Whether to inhibit errors raised when registering a new account if the user ID
1432+
# already exists. If turned on, that requests to /register/available will always
1433+
# show a user ID as available, and Synapse won't raise an error when starting
1434+
# a registration with a user ID that already exists. However, Synapse will still
1435+
# raise an error if the registration completes and the username conflicts.
1436+
#
1437+
# Defaults to false.
1438+
#
1439+
#inhibit_user_in_use_error: true
1440+
14311441

14321442
## Metrics ###
14331443

synapse/config/registration.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,8 @@ def read_config(self, config, **kwargs):
190190
# The success template used during fallback auth.
191191
self.fallback_success_template = self.read_template("auth_success.html")
192192

193+
self.inhibit_user_in_use_error = config.get("inhibit_user_in_use_error", False)
194+
193195
def generate_config_section(self, generate_secrets=False, **kwargs):
194196
if generate_secrets:
195197
registration_shared_secret = 'registration_shared_secret: "%s"' % (
@@ -446,6 +448,16 @@ def generate_config_section(self, generate_secrets=False, **kwargs):
446448
# Defaults to true.
447449
#
448450
#auto_join_rooms_for_guests: false
451+
452+
# Whether to inhibit errors raised when registering a new account if the user ID
453+
# already exists. If turned on, that requests to /register/available will always
454+
# show a user ID as available, and Synapse won't raise an error when starting
455+
# a registration with a user ID that already exists. However, Synapse will still
456+
# raise an error if the registration completes and the username conflicts.
457+
#
458+
# Defaults to false.
459+
#
460+
#inhibit_user_in_use_error: true
449461
"""
450462
% locals()
451463
)

synapse/handlers/register.py

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,7 @@ async def check_username(
132132
localpart: str,
133133
guest_access_token: Optional[str] = None,
134134
assigned_user_id: Optional[str] = None,
135+
inhibit_user_in_use_error: bool = False,
135136
) -> None:
136137
if types.contains_invalid_mxid_characters(localpart):
137138
raise SynapseError(
@@ -171,21 +172,22 @@ async def check_username(
171172

172173
users = await self.store.get_users_by_id_case_insensitive(user_id)
173174
if users:
174-
if not guest_access_token:
175+
if not inhibit_user_in_use_error and not guest_access_token:
175176
raise SynapseError(
176177
400, "User ID already taken.", errcode=Codes.USER_IN_USE
177178
)
178-
user_data = await self.auth.get_user_by_access_token(guest_access_token)
179-
if (
180-
not user_data.is_guest
181-
or UserID.from_string(user_data.user_id).localpart != localpart
182-
):
183-
raise AuthError(
184-
403,
185-
"Cannot register taken user ID without valid guest "
186-
"credentials for that user.",
187-
errcode=Codes.FORBIDDEN,
188-
)
179+
if guest_access_token:
180+
user_data = await self.auth.get_user_by_access_token(guest_access_token)
181+
if (
182+
not user_data.is_guest
183+
or UserID.from_string(user_data.user_id).localpart != localpart
184+
):
185+
raise AuthError(
186+
403,
187+
"Cannot register taken user ID without valid guest "
188+
"credentials for that user.",
189+
errcode=Codes.FORBIDDEN,
190+
)
189191

190192
if guest_access_token is None:
191193
try:

synapse/rest/client/register.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -339,12 +339,19 @@ def __init__(self, hs: "HomeServer"):
339339
),
340340
)
341341

342+
self.inhibit_user_in_use_error = (
343+
hs.config.registration.inhibit_user_in_use_error
344+
)
345+
342346
async def on_GET(self, request: Request) -> Tuple[int, JsonDict]:
343347
if not self.hs.config.registration.enable_registration:
344348
raise SynapseError(
345349
403, "Registration has been disabled", errcode=Codes.FORBIDDEN
346350
)
347351

352+
if self.inhibit_user_in_use_error:
353+
return 200, {"available": True}
354+
348355
ip = request.getClientIP()
349356
with self.ratelimiter.ratelimit(ip) as wait_deferred:
350357
await wait_deferred
@@ -422,6 +429,9 @@ def __init__(self, hs: "HomeServer"):
422429
self._refresh_tokens_enabled = (
423430
hs.config.registration.refreshable_access_token_lifetime is not None
424431
)
432+
self._inhibit_user_in_use_error = (
433+
hs.config.registration.inhibit_user_in_use_error
434+
)
425435

426436
self._registration_flows = _calculate_registration_flows(
427437
hs.config, self.auth_handler
@@ -564,6 +574,7 @@ async def on_POST(self, request: SynapseRequest) -> Tuple[int, JsonDict]:
564574
desired_username,
565575
guest_access_token=guest_access_token,
566576
assigned_user_id=registered_user_id,
577+
inhibit_user_in_use_error=self._inhibit_user_in_use_error,
567578
)
568579

569580
# Check if the user-interactive authentication flows are complete, if

tests/rest/client/test_register.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -726,6 +726,47 @@ def test_reject_invalid_email(self):
726726
{"errcode": "M_UNKNOWN", "error": "Unable to parse email address"},
727727
)
728728

729+
@override_config(
730+
{
731+
"inhibit_user_in_use_error": True,
732+
}
733+
)
734+
def test_inhibit_user_in_use_error(self):
735+
"""Tests that the 'inhibit_user_in_use_error' configuration flag behaves
736+
correctly.
737+
"""
738+
username = "arthur"
739+
740+
# Manually register the user, so we know the test isn't passing because of a lack
741+
# of clashing.
742+
reg_handler = self.hs.get_registration_handler()
743+
self.get_success(reg_handler.register_user(username))
744+
745+
# Check that /available correctly ignores the username provided despite the
746+
# username being already registered.
747+
channel = self.make_request("GET", "register/available?username=" + username)
748+
self.assertEquals(200, channel.code, channel.result)
749+
750+
# Test that when starting a UIA registration flow the request doesn't fail because
751+
# of a conflicting username
752+
channel = self.make_request(
753+
"POST",
754+
"register",
755+
{"username": username, "type": "m.login.password", "password": "foo"},
756+
)
757+
self.assertEqual(channel.code, 401)
758+
self.assertIn("session", channel.json_body)
759+
760+
# Test that finishing the registration fails because of a conflicting username.
761+
session = channel.json_body["session"]
762+
channel = self.make_request(
763+
"POST",
764+
"register",
765+
{"auth": {"session": session, "type": LoginType.DUMMY}},
766+
)
767+
self.assertEqual(channel.code, 400, channel.json_body)
768+
self.assertEqual(channel.json_body["errcode"], Codes.USER_IN_USE)
769+
729770

730771
class AccountValidityTestCase(unittest.HomeserverTestCase):
731772

0 commit comments

Comments
 (0)