summary refs log tree commit diff
diff options
context:
space:
mode:
authorAndrew Morgan <1342360+anoadragon453@users.noreply.github.com>2020-07-02 11:01:02 +0100
committerGitHub <noreply@github.com>2020-07-02 11:01:02 +0100
commit21821c076a9335efbdc13a5875fe548f3726b8bc (patch)
treeb4407227e3614580c484b74470c81144e0898a2a
parentMerge pull request #50 from matrix-org/dinsic-release-v1.15.x (diff)
downloadsynapse-21821c076a9335efbdc13a5875fe548f3726b8bc.tar.xz
Add option to autobind user's email on registration (#51)
Adds an option, `bind_new_user_emails_to_sydent`, which uses Sydent's [internal bind api](https://github.com/matrix-org/sydent#internal-bind-and-unbind-api) to automatically bind email addresses of users immediately after they register.

This is quite enterprise-specific, but could be generally useful to multiple organizations. This aims to solve the problem of requiring users to verify their email twice when using the functionality of an identity server in a corporate deployment - where both the homeserver and identity server are controlled. It does with while eliminating the need for the `account_threepid_delegates.email` option, which historically has been a very complicated option to reason about.
-rw-r--r--changelog.d/51.feature1
-rw-r--r--docs/sample_config.yaml18
-rw-r--r--synapse/config/registration.py35
-rw-r--r--synapse/handlers/identity.py24
-rw-r--r--synapse/handlers/register.py45
-rw-r--r--tests/handlers/test_register.py85
6 files changed, 183 insertions, 25 deletions
diff --git a/changelog.d/51.feature b/changelog.d/51.feature
new file mode 100644

index 0000000000..e5c9990ad6 --- /dev/null +++ b/changelog.d/51.feature
@@ -0,0 +1 @@ +Add `bind_new_user_emails_to_sydent` option for automatically binding user's emails after registration. diff --git a/docs/sample_config.yaml b/docs/sample_config.yaml
index 060c74c4a8..847926c146 100644 --- a/docs/sample_config.yaml +++ b/docs/sample_config.yaml
@@ -1387,6 +1387,24 @@ account_threepid_delegates: #rewrite_identity_server_urls: # "https://somewhere.example.com": "https://somewhereelse.example.com" +# When a user registers an account with an email address, it can be useful to +# bind that email address to their mxid on an identity server. Typically, this +# requires the user to validate their email address with the identity server. +# However if Synapse itself is handling email validation on registration, the +# user ends up needing to validate their email twice, which leads to poor UX. +# +# It is possible to force Sydent, one identity server implementation, to bind +# threepids using its internal, unauthenticated bind API: +# https://github.com/matrix-org/sydent/#internal-bind-and-unbind-api +# +# Configure the address of a Sydent server here to have Synapse attempt +# to automatically bind users' emails following registration. The +# internal bind API must be reachable from Synapse, but should NOT be +# exposed to any third party, as it allows the creation of bindings +# without validation. +# +#bind_new_user_emails_to_sydent: https://example.com:8091 + ## Metrics ### diff --git a/synapse/config/registration.py b/synapse/config/registration.py
index a46b3ef53e..43b87e9a70 100644 --- a/synapse/config/registration.py +++ b/synapse/config/registration.py
@@ -177,6 +177,23 @@ class RegistrationConfig(Config): session_lifetime = self.parse_duration(session_lifetime) self.session_lifetime = session_lifetime + self.bind_new_user_emails_to_sydent = config.get( + "bind_new_user_emails_to_sydent" + ) + + if self.bind_new_user_emails_to_sydent: + if not isinstance( + self.bind_new_user_emails_to_sydent, str + ) or not self.bind_new_user_emails_to_sydent.startswith("http"): + raise ConfigError( + "Option bind_new_user_emails_to_sydent has invalid value" + ) + + # Remove trailing slashes + self.bind_new_user_emails_to_sydent = self.bind_new_user_emails_to_sydent.strip( + "/" + ) + def generate_config_section(self, generate_secrets=False, **kwargs): if generate_secrets: registration_shared_secret = 'registration_shared_secret: "%s"' % ( @@ -469,6 +486,24 @@ class RegistrationConfig(Config): # #rewrite_identity_server_urls: # "https://somewhere.example.com": "https://somewhereelse.example.com" + + # When a user registers an account with an email address, it can be useful to + # bind that email address to their mxid on an identity server. Typically, this + # requires the user to validate their email address with the identity server. + # However if Synapse itself is handling email validation on registration, the + # user ends up needing to validate their email twice, which leads to poor UX. + # + # It is possible to force Sydent, one identity server implementation, to bind + # threepids using its internal, unauthenticated bind API: + # https://github.com/matrix-org/sydent/#internal-bind-and-unbind-api + # + # Configure the address of a Sydent server here to have Synapse attempt + # to automatically bind users' emails following registration. The + # internal bind API must be reachable from Synapse, but should NOT be + # exposed to any third party, as it allows the creation of bindings + # without validation. + # + #bind_new_user_emails_to_sydent: https://example.com:8091 """ % locals() ) diff --git a/synapse/handlers/identity.py b/synapse/handlers/identity.py
index 6039034c00..a77088e295 100644 --- a/synapse/handlers/identity.py +++ b/synapse/handlers/identity.py
@@ -1033,6 +1033,30 @@ class IdentityHandler(BaseHandler): display_name = data["display_name"] return token, public_keys, fallback_public_key, display_name + async def bind_email_using_internal_sydent_api( + self, id_server_url: str, email: str, user_id: str, + ): + """Bind an email to a fully qualified user ID using the internal API of an + instance of Sydent. + + Args: + id_server_url: The URL of the Sydent instance + email: The email address to bind + user_id: The user ID to bind the email to + + Raises: + HTTPResponseException: On a non-2xx HTTP response. + """ + # id_server_url is assumed to have no trailing slashes + url = id_server_url + "/_matrix/identity/internal/bind" + body = { + "address": email, + "medium": "email", + "mxid": user_id, + } + + await self.http_client.post_json_get_json(url, body) + def create_id_access_token_header(id_access_token): """Create an Authorization header for passing to SimpleHttpClient as the header value diff --git a/synapse/handlers/register.py b/synapse/handlers/register.py
index d5d44de8d0..99c1a78fd0 100644 --- a/synapse/handlers/register.py +++ b/synapse/handlers/register.py
@@ -636,6 +636,7 @@ class RegistrationHandler(BaseHandler): if auth_result and LoginType.EMAIL_IDENTITY in auth_result: threepid = auth_result[LoginType.EMAIL_IDENTITY] + # Necessary due to auth checks prior to the threepid being # written to the db if is_threepid_reserved( @@ -645,34 +646,30 @@ class RegistrationHandler(BaseHandler): await self.register_email_threepid(user_id, threepid, access_token) - if self.hs.config.account_threepid_delegate_email: - # Bind the 3PID to the identity server + if self.hs.config.bind_new_user_emails_to_sydent: + # Attempt to call Sydent's internal bind API on the given identity server + # to bind this threepid + id_server_url = self.hs.config.bind_new_user_emails_to_sydent + logger.debug( - "Binding email to %s on id_server %s", + "Attempting the bind email of %s to identity server: %s using " + "internal Sydent bind API.", user_id, - self.hs.config.account_threepid_delegate_email, + self.hs.config.bind_new_user_emails_to_sydent, ) - threepid_creds = threepid["threepid_creds"] - - # Remove the protocol scheme before handling to `bind_threepid` - # `bind_threepid` will add https:// to it, so this restricts - # account_threepid_delegate.email to https:// addresses only - # We assume this is always the case for dinsic however. - if self.hs.config.account_threepid_delegate_email.startswith( - "https://" - ): - id_server = self.hs.config.account_threepid_delegate_email[8:] - else: - # Must start with http:// instead - id_server = self.hs.config.account_threepid_delegate_email[7:] - await self.identity_handler.bind_threepid( - threepid_creds["client_secret"], - threepid_creds["sid"], - user_id, - id_server, - threepid_creds.get("id_access_token"), - ) + try: + await self.identity_handler.bind_email_using_internal_sydent_api( + id_server_url, threepid["address"], user_id + ) + except Exception as e: + logger.warning( + "Failed to bind email of '%s' to Sydent instance '%s' ", + "using Sydent internal bind API: %s", + user_id, + id_server_url, + e, + ) if auth_result and LoginType.MSISDN in auth_result: threepid = auth_result[LoginType.MSISDN] diff --git a/tests/handlers/test_register.py b/tests/handlers/test_register.py
index 2a377a4eb9..a7f52067d0 100644 --- a/tests/handlers/test_register.py +++ b/tests/handlers/test_register.py
@@ -20,9 +20,16 @@ from twisted.internet import defer from synapse.api.constants import UserTypes from synapse.api.errors import Codes, ResourceLimitError, SynapseError from synapse.handlers.register import RegistrationHandler -from synapse.rest.client.v2_alpha.register import _map_email_to_displayname +from synapse.http.site import SynapseRequest +from synapse.rest.client.v2_alpha.register import ( + _map_email_to_displayname, + register_servlets, +) from synapse.types import RoomAlias, UserID, create_requester +from tests.server import FakeChannel +from tests.unittest import override_config + from .. import unittest @@ -34,6 +41,10 @@ class RegistrationHandlers(object): class RegistrationTestCase(unittest.HomeserverTestCase): """ Tests the RegistrationHandler. """ + servlets = [ + register_servlets, + ] + def make_homeserver(self, reactor, clock): hs_config = self.default_config() @@ -287,6 +298,78 @@ class RegistrationTestCase(unittest.HomeserverTestCase): result = _map_email_to_displayname(i) self.assertEqual(result, expected) + @override_config( + { + "bind_new_user_emails_to_sydent": "https://is.example.com", + "registrations_require_3pid": ["email"], + "account_threepid_delegates": {}, + "email": { + "smtp_host": "127.0.0.1", + "smtp_port": 20, + "require_transport_security": False, + "smtp_user": None, + "smtp_pass": None, + "notif_from": "test@example.com", + }, + "public_baseurl": "http://localhost", + } + ) + def test_user_email_bound_via_sydent_internal_api(self): + """Tests that emails are bound after registration if this option is set""" + # Register user with an email address + email = "alice@example.com" + + # Mock Synapse's threepid validator + get_threepid_validation_session = Mock( + return_value=defer.succeed( + {"medium": "email", "address": email, "validated_at": 0} + ) + ) + self.store.get_threepid_validation_session = get_threepid_validation_session + delete_threepid_session = Mock(return_value=defer.succeed(None)) + self.store.delete_threepid_session = delete_threepid_session + + # Mock Synapse's http json post method to check for the internal bind call + post_json_get_json = Mock(return_value=defer.succeed(None)) + self.hs.get_simple_http_client().post_json_get_json = post_json_get_json + + # Retrieve a UIA session ID + channel = self.uia_register( + 401, {"username": "alice", "password": "nobodywillguessthis"} + ) + session_id = channel.json_body["session"] + + # Register our email address using the fake validation session above + channel = self.uia_register( + 200, + { + "username": "alice", + "password": "nobodywillguessthis", + "auth": { + "session": session_id, + "type": "m.login.email.identity", + "threepid_creds": {"sid": "blabla", "client_secret": "blablabla"}, + }, + }, + ) + self.assertEqual(channel.json_body["user_id"], "@alice:test") + + # Check that a bind attempt was made to our fake identity server + post_json_get_json.assert_called_with( + "https://is.example.com/_matrix/identity/internal/bind", + {"address": "alice@example.com", "medium": "email", "mxid": "@alice:test"}, + ) + + def uia_register(self, expected_response: int, body: dict) -> FakeChannel: + """Make a register request.""" + request, channel = self.make_request( + "POST", "register", body + ) # type: SynapseRequest, FakeChannel + self.render(request) + + self.assertEqual(request.code, expected_response) + return channel + async def get_or_create_user( self, requester, localpart, displayname, password_hash=None ):