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 (https://github.com/matrix-org/synapse/pull/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
This commit is contained in:
Brendan Abolivier 2022-01-26 12:02:54 +00:00 committed by GitHub
parent 74e4419eb4
commit 95b3f952fa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 89 additions and 12 deletions

View File

@ -0,0 +1 @@
Add a config flag to inhibit M_USER_IN_USE during registration.

View File

@ -1428,6 +1428,16 @@ account_threepid_delegates:
# #
#auto_join_rooms_for_guests: false #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 ### ## Metrics ###

View File

@ -190,6 +190,8 @@ class RegistrationConfig(Config):
# The success template used during fallback auth. # The success template used during fallback auth.
self.fallback_success_template = self.read_template("auth_success.html") 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): def generate_config_section(self, generate_secrets=False, **kwargs):
if generate_secrets: if generate_secrets:
registration_shared_secret = 'registration_shared_secret: "%s"' % ( registration_shared_secret = 'registration_shared_secret: "%s"' % (
@ -446,6 +448,16 @@ class RegistrationConfig(Config):
# Defaults to true. # Defaults to true.
# #
#auto_join_rooms_for_guests: false #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() % locals()
) )

View File

@ -132,6 +132,7 @@ class RegistrationHandler:
localpart: str, localpart: str,
guest_access_token: Optional[str] = None, guest_access_token: Optional[str] = None,
assigned_user_id: Optional[str] = None, assigned_user_id: Optional[str] = None,
inhibit_user_in_use_error: bool = False,
) -> None: ) -> None:
if types.contains_invalid_mxid_characters(localpart): if types.contains_invalid_mxid_characters(localpart):
raise SynapseError( raise SynapseError(
@ -171,21 +172,22 @@ class RegistrationHandler:
users = await self.store.get_users_by_id_case_insensitive(user_id) users = await self.store.get_users_by_id_case_insensitive(user_id)
if users: if users:
if not guest_access_token: if not inhibit_user_in_use_error and not guest_access_token:
raise SynapseError( raise SynapseError(
400, "User ID already taken.", errcode=Codes.USER_IN_USE 400, "User ID already taken.", errcode=Codes.USER_IN_USE
) )
user_data = await self.auth.get_user_by_access_token(guest_access_token) if guest_access_token:
if ( user_data = await self.auth.get_user_by_access_token(guest_access_token)
not user_data.is_guest if (
or UserID.from_string(user_data.user_id).localpart != localpart not user_data.is_guest
): or UserID.from_string(user_data.user_id).localpart != localpart
raise AuthError( ):
403, raise AuthError(
"Cannot register taken user ID without valid guest " 403,
"credentials for that user.", "Cannot register taken user ID without valid guest "
errcode=Codes.FORBIDDEN, "credentials for that user.",
) errcode=Codes.FORBIDDEN,
)
if guest_access_token is None: if guest_access_token is None:
try: try:

View File

@ -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]: async def on_GET(self, request: Request) -> Tuple[int, JsonDict]:
if not self.hs.config.registration.enable_registration: if not self.hs.config.registration.enable_registration:
raise SynapseError( raise SynapseError(
403, "Registration has been disabled", errcode=Codes.FORBIDDEN 403, "Registration has been disabled", errcode=Codes.FORBIDDEN
) )
if self.inhibit_user_in_use_error:
return 200, {"available": True}
ip = request.getClientIP() ip = request.getClientIP()
with self.ratelimiter.ratelimit(ip) as wait_deferred: with self.ratelimiter.ratelimit(ip) as wait_deferred:
await wait_deferred await wait_deferred
@ -422,6 +429,9 @@ class RegisterRestServlet(RestServlet):
self._refresh_tokens_enabled = ( self._refresh_tokens_enabled = (
hs.config.registration.refreshable_access_token_lifetime is not None 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( self._registration_flows = _calculate_registration_flows(
hs.config, self.auth_handler hs.config, self.auth_handler
@ -564,6 +574,7 @@ class RegisterRestServlet(RestServlet):
desired_username, desired_username,
guest_access_token=guest_access_token, guest_access_token=guest_access_token,
assigned_user_id=registered_user_id, 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 # Check if the user-interactive authentication flows are complete, if

View File

@ -726,6 +726,47 @@ class RegisterRestServletTestCase(unittest.HomeserverTestCase):
{"errcode": "M_UNKNOWN", "error": "Unable to parse email address"}, {"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): class AccountValidityTestCase(unittest.HomeserverTestCase):