diff --git a/changelog.d/18262.feature b/changelog.d/18262.feature
new file mode 100644
index 0000000000..c8249faa76
--- /dev/null
+++ b/changelog.d/18262.feature
@@ -0,0 +1 @@
+Add option to allow registrations that begin with `_`. Contributed by `_` (@hex5f).
diff --git a/docs/usage/configuration/config_documentation.md b/docs/usage/configuration/config_documentation.md
index 2228c18a6c..e688bc5cd8 100644
--- a/docs/usage/configuration/config_documentation.md
+++ b/docs/usage/configuration/config_documentation.md
@@ -2887,6 +2887,20 @@ Example configuration:
inhibit_user_in_use_error: true
```
---
+### `allow_underscore_prefixed_registration`
+
+Whether users are allowed to register with a underscore-prefixed localpart.
+By default, AppServices use prefixes like `_example` to namespace their
+associated ghost users. If turned on, this may result in clashes or confusion.
+Useful when provisioning users from an external identity provider.
+
+Defaults to false.
+
+Example configuration:
+```yaml
+allow_underscore_prefixed_registration: false
+```
+---
## User session management
---
### `session_lifetime`
diff --git a/synapse/config/registration.py b/synapse/config/registration.py
index 3cf7031656..8adf21079e 100644
--- a/synapse/config/registration.py
+++ b/synapse/config/registration.py
@@ -162,6 +162,10 @@ class RegistrationConfig(Config):
"disable_msisdn_registration", False
)
+ self.allow_underscore_prefixed_localpart = config.get(
+ "allow_underscore_prefixed_localpart", False
+ )
+
session_lifetime = config.get("session_lifetime")
if session_lifetime is not None:
session_lifetime = self.parse_duration(session_lifetime)
diff --git a/synapse/handlers/register.py b/synapse/handlers/register.py
index ecfea175c7..3e86349981 100644
--- a/synapse/handlers/register.py
+++ b/synapse/handlers/register.py
@@ -159,7 +159,10 @@ class RegistrationHandler:
if not localpart:
raise SynapseError(400, "User ID cannot be empty", Codes.INVALID_USERNAME)
- if localpart[0] == "_":
+ if (
+ localpart[0] == "_"
+ and not self.hs.config.registration.allow_underscore_prefixed_localpart
+ ):
raise SynapseError(
400, "User ID may not begin with _", Codes.INVALID_USERNAME
)
diff --git a/tests/handlers/test_register.py b/tests/handlers/test_register.py
index 92487692db..dda389c08b 100644
--- a/tests/handlers/test_register.py
+++ b/tests/handlers/test_register.py
@@ -588,6 +588,29 @@ class RegistrationTestCase(unittest.HomeserverTestCase):
d = self.store.is_support_user(user_id)
self.assertFalse(self.get_success(d))
+ def test_underscore_localpart_rejected_by_default(self) -> None:
+ for invalid_user_id in ("_", "_prefixed"):
+ with self.subTest(invalid_user_id=invalid_user_id):
+ self.get_failure(
+ self.handler.register_user(localpart=invalid_user_id),
+ SynapseError,
+ )
+
+ @override_config(
+ {
+ "allow_underscore_prefixed_localpart": True,
+ }
+ )
+ def test_underscore_localpart_allowed_if_configured(self) -> None:
+ for valid_user_id in ("_", "_prefixed"):
+ with self.subTest(valid_user_id=valid_user_id):
+ user_id = self.get_success(
+ self.handler.register_user(
+ localpart=valid_user_id,
+ ),
+ )
+ self.assertEqual(user_id, f"@{valid_user_id}:test")
+
def test_invalid_user_id(self) -> None:
invalid_user_id = "^abcd"
self.get_failure(
|