diff --git a/changelog.d/5876.misc b/changelog.d/5876.misc new file mode 100644 index 000000000000..c1c289d05a26 --- /dev/null +++ b/changelog.d/5876.misc @@ -0,0 +1 @@ +Replace `trust_identity_server_for_password_resets` config option with `account_threepid_delegate`. \ No newline at end of file diff --git a/docs/sample_config.yaml b/docs/sample_config.yaml index c208f7f4bd93..81b24188050d 100644 --- a/docs/sample_config.yaml +++ b/docs/sample_config.yaml @@ -902,6 +902,20 @@ uploads_path: "DATADIR/uploads" # - matrix.org # - vector.im +# Handle threepid (email/phone etc) registration and password resets +# through a *trusted* identity server. Note that this allows the configured +# identity server to reset passwords for accounts. +# +# If this option is not defined and SMTP options have not been +# configured, registration by email and resetting user passwords via +# email will be disabled +# +# Otherwise, to enable set this option to the reachable domain name, including protocol +# definition, for an identity server +# (e.g "https://matrix.org", "http://localhost:8090") +# +#account_threepid_delegate: "" + # Users who register on this homeserver will automatically be joined # to these rooms # @@ -1163,19 +1177,6 @@ password_config: # # # riot_base_url: "http://localhost/riot" # -# # Enable sending password reset emails via the configured, trusted -# # identity servers -# # -# # IMPORTANT! This will give a malicious or overtaken identity server -# # the ability to reset passwords for your users! Make absolutely sure -# # that you want to do this! It is strongly recommended that password -# # reset emails be sent by the homeserver instead -# # -# # If this option is set to false and SMTP options have not been -# # configured, resetting user passwords via email will be disabled -# # -# #trust_identity_server_for_password_resets: false -# # # Configure the time that a validation email or text message code # # will expire after sending # # diff --git a/synapse/config/emailconfig.py b/synapse/config/emailconfig.py index 36d01a10af70..58c73ff70f0d 100644 --- a/synapse/config/emailconfig.py +++ b/synapse/config/emailconfig.py @@ -20,6 +20,7 @@ # This file can't be called email.py because if it is, we cannot: import email.utils import os +from enum import Enum import pkg_resources @@ -74,19 +75,39 @@ def read_config(self, config, **kwargs): "renew_at" ) - email_trust_identity_server_for_password_resets = email_config.get( - "trust_identity_server_for_password_resets", False + self.email_threepid_behaviour = ( + # Have Synapse handle the email sending if account_threepid_delegate + # is not defined + ThreepidBehaviour.REMOTE + if self.account_threepid_delegate + else ThreepidBehaviour.LOCAL ) - self.email_password_reset_behaviour = ( - "remote" if email_trust_identity_server_for_password_resets else "local" - ) - self.password_resets_were_disabled_due_to_email_config = False - if self.email_password_reset_behaviour == "local" and email_config == {}: + # Prior to Synapse v1.4.0, there was another option that defined whether Synapse would + # use an identity server to password reset tokens on its behalf. We now warn the user + # if they have this set and tell them to use the updated option, while using a default + # identity server in the process. + self.using_identity_server_from_trusted_list = False + if config.get("trust_identity_server_for_password_resets", False) is True: + # Use the first entry in self.trusted_third_party_id_servers instead + if self.trusted_third_party_id_servers: + self.account_threepid_delegate = self.trusted_third_party_id_servers[0] + self.using_identity_server_from_trusted_list = True + else: + raise ConfigError( + "Attempted to use an identity server from" + '"trusted_third_party_id_servers" but it is empty.' + ) + + self.local_threepid_emails_disabled_due_to_config = False + if ( + self.email_threepid_behaviour == ThreepidBehaviour.LOCAL + and email_config == {} + ): # We cannot warn the user this has happened here # Instead do so when a user attempts to reset their password - self.password_resets_were_disabled_due_to_email_config = True + self.local_threepid_emails_disabled_due_to_config = True - self.email_password_reset_behaviour = "off" + self.email_threepid_behaviour = ThreepidBehaviour.OFF # Get lifetime of a validation token in milliseconds self.email_validation_token_lifetime = self.parse_duration( @@ -96,7 +117,7 @@ def read_config(self, config, **kwargs): if ( self.email_enable_notifs or account_validity_renewal_enabled - or self.email_password_reset_behaviour == "local" + or self.email_threepid_behaviour == ThreepidBehaviour.LOCAL ): # make sure we can import the required deps import jinja2 @@ -106,7 +127,7 @@ def read_config(self, config, **kwargs): jinja2 bleach - if self.email_password_reset_behaviour == "local": + if self.email_threepid_behaviour == ThreepidBehaviour.LOCAL: required = ["smtp_host", "smtp_port", "notif_from"] missing = [] @@ -239,19 +260,6 @@ def generate_config_section(self, config_dir_path, server_name, **kwargs): # # # riot_base_url: "http://localhost/riot" # - # # Enable sending password reset emails via the configured, trusted - # # identity servers - # # - # # IMPORTANT! This will give a malicious or overtaken identity server - # # the ability to reset passwords for your users! Make absolutely sure - # # that you want to do this! It is strongly recommended that password - # # reset emails be sent by the homeserver instead - # # - # # If this option is set to false and SMTP options have not been - # # configured, resetting user passwords via email will be disabled - # # - # #trust_identity_server_for_password_resets: false - # # # Configure the time that a validation email or text message code # # will expire after sending # # @@ -289,3 +297,18 @@ def generate_config_section(self, config_dir_path, server_name, **kwargs): # #password_reset_template_success_html: password_reset_success.html # #password_reset_template_failure_html: password_reset_failure.html """ + + +class ThreepidBehaviour(Enum): + """ + Enum to define the behaviour of Synapse with regards to when it contacts an identity + server for 3pid registration and password resets + + REMOTE = use an external server to send tokens + LOCAL = send tokens ourselves + OFF = disable registration via 3pid and password resets + """ + + REMOTE = "remote" + LOCAL = "local" + OFF = "off" diff --git a/synapse/config/registration.py b/synapse/config/registration.py index df3491568c1f..b9d5e81b1dd5 100644 --- a/synapse/config/registration.py +++ b/synapse/config/registration.py @@ -99,6 +99,7 @@ def read_config(self, config, **kwargs): self.trusted_third_party_id_servers = config.get( "trusted_third_party_id_servers", ["matrix.org", "vector.im"] ) + self.account_threepid_delegate = config.get("account_threepid_delegate") self.default_identity_server = config.get("default_identity_server") self.allow_guest_access = config.get("allow_guest_access", False) @@ -269,6 +270,20 @@ def generate_config_section(self, generate_secrets=False, **kwargs): # - matrix.org # - vector.im + # Handle threepid (email/phone etc) registration and password resets + # through a *trusted* identity server. Note that this allows the configured + # identity server to reset passwords for accounts. + # + # If this option is not defined and SMTP options have not been + # configured, registration by email and resetting user passwords via + # email will be disabled + # + # Otherwise, to enable set this option to the reachable domain name, including protocol + # definition, for an identity server + # (e.g "https://matrix.org", "http://localhost:8090") + # + #account_threepid_delegate: "" + # Users who register on this homeserver will automatically be joined # to these rooms # diff --git a/synapse/handlers/auth.py b/synapse/handlers/auth.py index 0f3ebf7ef887..091512aa536d 100644 --- a/synapse/handlers/auth.py +++ b/synapse/handlers/auth.py @@ -38,6 +38,7 @@ UserDeactivatedError, ) from synapse.api.ratelimiting import Ratelimiter +from synapse.config.emailconfig import ThreepidBehaviour from synapse.logging.context import defer_to_thread from synapse.module_api import ModuleApi from synapse.types import UserID @@ -460,10 +461,10 @@ def _check_threepid(self, medium, authdict, password_servlet=False, **kwargs): logger.info("Getting validated threepid. threepidcreds: %r", (threepid_creds,)) if ( not password_servlet - or self.hs.config.email_password_reset_behaviour == "remote" + or self.hs.config.email_threepid_behaviour == ThreepidBehaviour.REMOTE ): threepid = yield identity_handler.threepid_from_creds(threepid_creds) - elif self.hs.config.email_password_reset_behaviour == "local": + elif self.hs.config.email_threepid_behaviour == ThreepidBehaviour.LOCAL: row = yield self.store.get_threepid_validation_session( medium, threepid_creds["client_secret"], diff --git a/synapse/handlers/identity.py b/synapse/handlers/identity.py index f342ad1bfb5f..6bdd1c18c37d 100644 --- a/synapse/handlers/identity.py +++ b/synapse/handlers/identity.py @@ -36,6 +36,7 @@ def __init__(self, hs): self.http_client = hs.get_simple_http_client() self.federation_http_client = hs.get_http_client() + self.hs = hs @defer.inlineCallbacks def threepid_from_creds(self, creds): @@ -199,19 +200,40 @@ def try_unbind_threepid_with_id_server(self, mxid, threepid, id_server): def requestEmailToken( self, id_server, email, client_secret, send_attempt, next_link=None ): + """ + Request an external server send an email on our behalf for the purposes of threepid + validation. + + Args: + id_server (str): The identity server to proxy to + email (str): The email to send the message to + client_secret (str): The unique client_secret sends by the user + send_attempt (int): Which attempt this is + next_link: A link to redirect the user to once they submit the token + + Returns: + The json response body from the server + """ params = { "email": email, "client_secret": client_secret, "send_attempt": send_attempt, } - if next_link: - params.update({"next_link": next_link}) + params["next_link"] = next_link + + if self.hs.config.using_identity_server_from_trusted_list: + # Warn that a deprecated config option is in use + logger.warn( + 'The config option "trust_identity_server_for_password_resets" ' + 'has been replaced by "account_threepid_delegate". ' + "Please consult the sample config at docs/sample_config.yaml for " + "details and update your config file." + ) try: data = yield self.http_client.post_json_get_json( - "https://%s%s" - % (id_server, "/_matrix/identity/api/v1/validate/email/requestToken"), + id_server + "/_matrix/identity/api/v1/validate/email/requestToken", params, ) return data @@ -221,20 +243,49 @@ def requestEmailToken( @defer.inlineCallbacks def requestMsisdnToken( - self, id_server, country, phone_number, client_secret, send_attempt, **kwargs + self, + id_server, + country, + phone_number, + client_secret, + send_attempt, + next_link=None, ): + """ + Request an external server send an SMS message on our behalf for the purposes of + threepid validation. + Args: + id_server (str): The identity server to proxy to + country (str): The country code of the phone number + phone_number (str): The number to send the message to + client_secret (str): The unique client_secret sends by the user + send_attempt (int): Which attempt this is + next_link: A link to redirect the user to once they submit the token + + Returns: + The json response body from the server + """ params = { "country": country, "phone_number": phone_number, "client_secret": client_secret, "send_attempt": send_attempt, } - params.update(kwargs) + if next_link: + params["next_link"] = next_link + + if self.hs.config.using_identity_server_from_trusted_list: + # Warn that a deprecated config option is in use + logger.warn( + 'The config option "trust_identity_server_for_password_resets" ' + 'has been replaced by "account_threepid_delegate". ' + "Please consult the sample config at docs/sample_config.yaml for " + "details and update your config file." + ) try: data = yield self.http_client.post_json_get_json( - "https://%s%s" - % (id_server, "/_matrix/identity/api/v1/validate/msisdn/requestToken"), + id_server + "/_matrix/identity/api/v1/validate/msisdn/requestToken", params, ) return data diff --git a/synapse/rest/client/v2_alpha/account.py b/synapse/rest/client/v2_alpha/account.py index 934ed5d16d1f..2c649259a20b 100644 --- a/synapse/rest/client/v2_alpha/account.py +++ b/synapse/rest/client/v2_alpha/account.py @@ -24,6 +24,7 @@ from synapse.api.constants import LoginType from synapse.api.errors import Codes, SynapseError, ThreepidValidationError +from synapse.config.emailconfig import ThreepidBehaviour from synapse.http.server import finish_request from synapse.http.servlet import ( RestServlet, @@ -50,7 +51,7 @@ def __init__(self, hs): self.config = hs.config self.identity_handler = hs.get_handlers().identity_handler - if self.config.email_password_reset_behaviour == "local": + if self.config.email_threepid_behaviour == ThreepidBehaviour.LOCAL: from synapse.push.mailer import Mailer, load_jinja2_templates templates = load_jinja2_templates( @@ -67,8 +68,8 @@ def __init__(self, hs): @defer.inlineCallbacks def on_POST(self, request): - if self.config.email_password_reset_behaviour == "off": - if self.config.password_resets_were_disabled_due_to_email_config: + if self.config.email_threepid_behaviour == ThreepidBehaviour.OFF: + if self.config.local_threepid_emails_disabled_due_to_config: logger.warn( "User password resets have been disabled due to lack of email config" ) @@ -93,22 +94,32 @@ def on_POST(self, request): Codes.THREEPID_DENIED, ) - existingUid = yield self.hs.get_datastore().get_user_id_by_threepid( + existing_user_id = yield self.hs.get_datastore().get_user_id_by_threepid( "email", email ) - if existingUid is None: + if existing_user_id is None: raise SynapseError(400, "Email not found", Codes.THREEPID_NOT_FOUND) - if self.config.email_password_reset_behaviour == "remote": - if "id_server" not in body: - raise SynapseError(400, "Missing 'id_server' param in body") + if self.config.email_threepid_behaviour == ThreepidBehaviour.REMOTE: + # Have the configured identity server handle the request + if not self.hs.config.account_threepid_delegate: + logger.warn( + "No upstream account_threepid_delegate configured on the server to handle " + "this request" + ) + raise SynapseError( + 400, "Password reset by email is not supported on this homeserver" + ) - # Have the identity server handle the password reset flow ret = yield self.identity_handler.requestEmailToken( - body["id_server"], email, client_secret, send_attempt, next_link + self.hs.config.account_threepid_delegate, + email, + client_secret, + send_attempt, + next_link, ) - else: + elif self.config.email_threepid_behaviour == ThreepidBehaviour.LOCAL: # Send password reset emails from Synapse sid = yield self.send_password_reset( email, client_secret, send_attempt, next_link @@ -116,6 +127,10 @@ def on_POST(self, request): # Wrap the session id in a JSON object ret = {"sid": sid} + else: + raise SynapseError( + 400, "Password reset by email is not supported on this homeserver" + ) return (200, ret) @@ -127,6 +142,8 @@ def send_password_reset(self, email, client_secret, send_attempt, next_link=None email (str): The user's email address client_secret (str): The provided client secret send_attempt (int): Which send attempt this is + next_link (str|None): The link to redirect the user to upon success. No redirect + occurs if None Returns: The new session_id upon success @@ -202,11 +219,15 @@ def on_POST(self, request): body = parse_json_object_from_request(request) assert_params_in_dict( - body, - ["id_server", "client_secret", "country", "phone_number", "send_attempt"], + body, ["client_secret", "country", "phone_number", "send_attempt"] ) + client_secret = body["client_secret"] + country = body["country"] + phone_number = body["phone_number"] + send_attempt = body["send_attempt"] + next_link = body.get("next_link") # Optional param - msisdn = phone_number_to_msisdn(body["country"], body["phone_number"]) + msisdn = phone_number_to_msisdn(country, phone_number) if not check_3pid_allowed(self.hs, "msisdn", msisdn): raise SynapseError( @@ -215,13 +236,37 @@ def on_POST(self, request): Codes.THREEPID_DENIED, ) - existingUid = yield self.datastore.get_user_id_by_threepid("msisdn", msisdn) + existing_user_id = yield self.datastore.get_user_id_by_threepid( + "msisdn", msisdn + ) - if existingUid is None: + if existing_user_id is None: raise SynapseError(400, "MSISDN not found", Codes.THREEPID_NOT_FOUND) - ret = yield self.identity_handler.requestMsisdnToken(**body) - return (200, ret) + if self.config.email_threepid_behaviour == ThreepidBehaviour.REMOTE: + if not self.hs.config.account_threepid_delegate: + logger.warn( + "No upstream account_threepid_delegate configured on the server to handle " + "this request" + ) + raise SynapseError( + 400, + "Password reset by phone number is not supported on this homeserver", + ) + + ret = yield self.identity_handler.requestMsisdnToken( + self.config.account_threepid_delegate, + country, + phone_number, + client_secret, + send_attempt, + next_link, + ) + return (200, ret) + + raise SynapseError( + 400, "Password reset by phone number is not supported on this homeserver" + ) class PasswordResetSubmitTokenServlet(RestServlet): @@ -249,8 +294,8 @@ def on_GET(self, request, medium): raise SynapseError( 400, "This medium is currently not supported for password resets" ) - if self.config.email_password_reset_behaviour == "off": - if self.config.password_resets_were_disabled_due_to_email_config: + if self.config.email_threepid_behaviour == ThreepidBehaviour.OFF: + if self.config.local_threepid_emails_disabled_due_to_config: logger.warn( "User password resets have been disabled due to lack of email config" ) @@ -457,7 +502,7 @@ def __init__(self, hs): self.hs = hs super(EmailThreepidRequestTokenRestServlet, self).__init__() self.identity_handler = hs.get_handlers().identity_handler - self.datastore = self.hs.get_datastore() + self.store = self.hs.get_datastore() @defer.inlineCallbacks def on_POST(self, request): @@ -465,6 +510,11 @@ def on_POST(self, request): assert_params_in_dict( body, ["id_server", "client_secret", "email", "send_attempt"] ) + id_server = "https://" + body["id_server"] # Assume https + client_secret = body["client_secret"] + email = body["email"] + send_attempt = body["send_attempt"] + next_link = body.get("next_link") # Optional param if not check_3pid_allowed(self.hs, "email", body["email"]): raise SynapseError( @@ -473,14 +523,16 @@ def on_POST(self, request): Codes.THREEPID_DENIED, ) - existingUid = yield self.datastore.get_user_id_by_threepid( + existing_user_id = yield self.store.get_user_id_by_threepid( "email", body["email"] ) - if existingUid is not None: + if existing_user_id is not None: raise SynapseError(400, "Email is already in use", Codes.THREEPID_IN_USE) - ret = yield self.identity_handler.requestEmailToken(**body) + ret = yield self.identity_handler.requestEmailToken( + id_server, email, client_secret, send_attempt, next_link + ) return (200, ret) @@ -491,7 +543,7 @@ def __init__(self, hs): self.hs = hs super(MsisdnThreepidRequestTokenRestServlet, self).__init__() self.identity_handler = hs.get_handlers().identity_handler - self.datastore = self.hs.get_datastore() + self.store = self.hs.get_datastore() @defer.inlineCallbacks def on_POST(self, request): @@ -500,8 +552,14 @@ def on_POST(self, request): body, ["id_server", "client_secret", "country", "phone_number", "send_attempt"], ) + id_server = "https://" + body["id_server"] # Assume https + client_secret = body["client_secret"] + country = body["country"] + phone_number = body["phone_number"] + send_attempt = body["send_attempt"] + next_link = body.get("next_link") # Optional param - msisdn = phone_number_to_msisdn(body["country"], body["phone_number"]) + msisdn = phone_number_to_msisdn(country, phone_number) if not check_3pid_allowed(self.hs, "msisdn", msisdn): raise SynapseError( @@ -510,12 +568,14 @@ def on_POST(self, request): Codes.THREEPID_DENIED, ) - existingUid = yield self.datastore.get_user_id_by_threepid("msisdn", msisdn) + existing_user_id = yield self.store.get_user_id_by_threepid("msisdn", msisdn) - if existingUid is not None: + if existing_user_id is not None: raise SynapseError(400, "MSISDN is already in use", Codes.THREEPID_IN_USE) - ret = yield self.identity_handler.requestMsisdnToken(**body) + ret = yield self.identity_handler.requestMsisdnToken( + id_server, country, phone_number, client_secret, send_attempt, next_link + ) return (200, ret) diff --git a/synapse/rest/client/v2_alpha/register.py b/synapse/rest/client/v2_alpha/register.py index 05ea1459e356..d866cdb1c7c6 100644 --- a/synapse/rest/client/v2_alpha/register.py +++ b/synapse/rest/client/v2_alpha/register.py @@ -31,6 +31,7 @@ SynapseError, UnrecognizedRequestError, ) +from synapse.config.emailconfig import ThreepidBehaviour from synapse.config.ratelimiting import FederationRateLimitConfig from synapse.config.server import is_threepid_reserved from synapse.http.servlet import ( @@ -76,9 +77,13 @@ def __init__(self, hs): def on_POST(self, request): body = parse_json_object_from_request(request) - assert_params_in_dict( - body, ["id_server", "client_secret", "email", "send_attempt"] - ) + assert_params_in_dict(body, ["client_secret", "email", "send_attempt"]) + + # Extract params from body + client_secret = body["client_secret"] + email = body["email"] + send_attempt = body["send_attempt"] + next_link = body.get("next_link") # Optional param if not check_3pid_allowed(self.hs, "email", body["email"]): raise SynapseError( @@ -87,14 +92,30 @@ def on_POST(self, request): Codes.THREEPID_DENIED, ) - existingUid = yield self.hs.get_datastore().get_user_id_by_threepid( + existing_user_id = yield self.hs.get_datastore().get_user_id_by_threepid( "email", body["email"] ) - if existingUid is not None: + if existing_user_id is not None: raise SynapseError(400, "Email is already in use", Codes.THREEPID_IN_USE) - ret = yield self.identity_handler.requestEmailToken(**body) + if not self.hs.config.account_threepid_delegate: + logger.warn( + "No upstream account_threepid_delegate configured on the server to handle " + "this request" + ) + raise SynapseError( + 400, "Registration by email is not supported on this homeserver" + ) + + ret = yield self.identity_handler.requestEmailToken( + self.hs.config.account_threepid_delegate, + email, + client_secret, + send_attempt, + next_link, + ) + return (200, ret) @@ -115,11 +136,15 @@ def on_POST(self, request): body = parse_json_object_from_request(request) assert_params_in_dict( - body, - ["id_server", "client_secret", "country", "phone_number", "send_attempt"], + body, ["client_secret", "country", "phone_number", "send_attempt"] ) + client_secret = body["client_secret"] + country = body["country"] + phone_number = body["phone_number"] + send_attempt = body["send_attempt"] + next_link = body.get("next_link") # Optional param - msisdn = phone_number_to_msisdn(body["country"], body["phone_number"]) + msisdn = phone_number_to_msisdn(country, phone_number) if not check_3pid_allowed(self.hs, "msisdn", msisdn): raise SynapseError( @@ -128,17 +153,39 @@ def on_POST(self, request): Codes.THREEPID_DENIED, ) - existingUid = yield self.hs.get_datastore().get_user_id_by_threepid( + existing_user_id = yield self.hs.get_datastore().get_user_id_by_threepid( "msisdn", msisdn ) - if existingUid is not None: + if existing_user_id is not None: raise SynapseError( 400, "Phone number is already in use", Codes.THREEPID_IN_USE ) - ret = yield self.identity_handler.requestMsisdnToken(**body) - return (200, ret) + if self.config.email_threepid_behaviour == ThreepidBehaviour.REMOTE: + if not self.hs.config.account_threepid_delegate: + logger.warn( + "No upstream account_threepid_delegate configured on the server to handle " + "this request" + ) + raise SynapseError( + 400, + "Registration by phone number is not supported on this homeserver", + ) + + ret = yield self.identity_handler.requestMsisdnToken( + self.config.account_threepid_delegate, + country, + phone_number, + client_secret, + send_attempt, + next_link, + ) + return (200, ret) + + raise SynapseError( + 400, "Registration by phone number is not supported on this homeserver" + ) class UsernameAvailabilityRestServlet(RestServlet): @@ -453,11 +500,11 @@ def on_POST(self, request): medium = auth_result[login_type]["medium"] address = auth_result[login_type]["address"] - existingUid = yield self.store.get_user_id_by_threepid( + existing_user_id = yield self.store.get_user_id_by_threepid( medium, address ) - if existingUid is not None: + if existing_user_id is not None: raise SynapseError( 400, "%s is already in use" % medium,