diff --git a/UPGRADE.rst b/UPGRADE.rst index cf228c7c529b..99e8da4b525f 100644 --- a/UPGRADE.rst +++ b/UPGRADE.rst @@ -49,6 +49,55 @@ returned by the Client-Server API: # configured on port 443. curl -kv https:///_matrix/client/versions 2>&1 | grep "Server:" +Upgrading to v1.4.0 +=================== + +Config options +-------------- + +**Note: Registration by email address or phone number will not work in this release unless +some config options are changed from their defaults.** + +This is due to Synapse v1.4.0 now defaulting to sending registration and password reset tokens +itself. This is for security reasons as well as putting less reliance on identity servers. +However, currently Synapse only supports sending emails, and does not have support for +phone-based password reset or account registration. If Synapse is configured to handle these on +its own, phone-based password resets and registration will be disabled. For Synapse to send +emails, the ``email`` block of the config must be filled out. If not, then password resets and +registration via email will be disabled entirely. + +This release also deprecates the ``email.trust_identity_server_for_password_resets`` option +and replaces it with ``account_threepid_delegate``. This option defines whether the homeserver +should delegate an external server (typically an `identity server +`_) to handle sending password reset +or registration messages via email or SMS. + +If ``email.trust_identity_server_for_password_resets`` was changed from its default to +``true``, and ``account_threepid_delegate`` is not set to an identity server domain, then the +server handling password resets and registration via third-party addresses will be set to the +first entry in the Synapse config's ``trusted_third_party_id_servers`` entry. If no domains are +configured, Synapse will throw an error on startup. + +If ``email.trust_identity_server_for_password_resets`` is not set to ``true`` and +``account_threepid_delegate`` is not set to a domain, then Synapse will attempt to send +password reset and registration messages itself. + +Email templates +--------------- + +If you have configured a custom template directory with the ``email.template_dir`` option, be +aware that there are new templates regarding registration. ``registration.html`` and +``registration.txt`` have been added and contain the text that is sent to a client upon +registering via email address. + +``registration_success.html`` and ``registration_failure.html`` are templates containing HTML +that will be shown to the user when they click the link in their registration email (if a +redirect URL is not configured), either showing them a success or failure page. + +Synapse will expect these files to exist inside the configured template directory. To view the +default templates, see `synapse/res/templates +`_. + Upgrading to v1.2.0 =================== @@ -132,6 +181,19 @@ server for password resets, set ``trust_identity_server_for_password_resets`` to See the `sample configuration file `_ for more details on these settings. +New email templates +--------------- +Some new templates have been added to the default template directory for the purpose of the +homeserver sending its own password reset emails. If you have configured a custom +``template_dir`` in your Synapse config, these files will need to be added. + +``password_reset.html`` and ``password_reset.txt`` are HTML and plain text templates +respectively that contain the contents of what will be emailed to the user upon attempting to +reset their password via email. ``password_reset_success.html`` and +``password_reset_failure.html`` are HTML files that the content of which (assuming no redirect +URL is set) will be shown to the user after they attempt to click the link in the email sent +to them. + Upgrading to v0.99.0 ==================== diff --git a/changelog.d/5835.feature b/changelog.d/5835.feature new file mode 100644 index 000000000000..3e8bf5068d02 --- /dev/null +++ b/changelog.d/5835.feature @@ -0,0 +1 @@ +Add the ability to send registration emails from the homeserver rather than delegating to an identity server. diff --git a/docs/sample_config.yaml b/docs/sample_config.yaml index b9e026115e47..8603008ec048 100644 --- a/docs/sample_config.yaml +++ b/docs/sample_config.yaml @@ -1217,11 +1217,22 @@ password_config: # #password_reset_template_html: password_reset.html # #password_reset_template_text: password_reset.txt # +# # Templates for registration emails sent by the homeserver +# # +# #registration_template_html: registration.html +# #registration_template_text: registration.txt +# # # Templates for password reset success and failure pages that a user # # will see after attempting to reset their password # # # #password_reset_template_success_html: password_reset_success.html # #password_reset_template_failure_html: password_reset_failure.html +# +# # Templates for registration success and failure pages that a user +# # will see after attempting to register using an email or phone +# # +# #registration_template_success_html: registration_success.html +# #registration_template_failure_html: registration_failure.html #password_providers: diff --git a/synapse/config/emailconfig.py b/synapse/config/emailconfig.py index d7b59faa3f26..874166b57938 100644 --- a/synapse/config/emailconfig.py +++ b/synapse/config/emailconfig.py @@ -75,7 +75,7 @@ def read_config(self, config, **kwargs): "renew_at" ) - self.email_threepid_behaviour = ( + self.threepid_behaviour = ( # Have Synapse handle the email sending if account_threepid_delegate # is not defined ThreepidBehaviour.REMOTE @@ -87,9 +87,14 @@ def read_config(self, config, **kwargs): # 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: + if ( + not self.account_threepid_delegate + and 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: + # XXX: It's a little confusing that account_threepid_delegate is modifed + # both in RegistrationConfig and here. We should factor this bit out self.account_threepid_delegate = self.trusted_third_party_id_servers[0] self.using_identity_server_from_trusted_list = True else: @@ -98,16 +103,13 @@ def read_config(self, config, **kwargs): '"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 == {} - ): + self.local_threepid_handling_disabled_due_to_email_config = False + if self.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.local_threepid_emails_disabled_due_to_config = True + self.local_threepid_handling_disabled_due_to_email_config = True - self.email_threepid_behaviour = ThreepidBehaviour.OFF + self.threepid_behaviour = ThreepidBehaviour.OFF # Get lifetime of a validation token in milliseconds self.email_validation_token_lifetime = self.parse_duration( @@ -117,7 +119,7 @@ def read_config(self, config, **kwargs): if ( self.email_enable_notifs or account_validity_renewal_enabled - or self.email_threepid_behaviour == ThreepidBehaviour.LOCAL + or self.threepid_behaviour == ThreepidBehaviour.LOCAL ): # make sure we can import the required deps import jinja2 @@ -127,7 +129,7 @@ def read_config(self, config, **kwargs): jinja2 bleach - if self.email_threepid_behaviour == ThreepidBehaviour.LOCAL: + if self.threepid_behaviour == ThreepidBehaviour.LOCAL: required = ["smtp_host", "smtp_port", "notif_from"] missing = [] @@ -146,28 +148,45 @@ def read_config(self, config, **kwargs): % (", ".join(missing),) ) - # Templates for password reset emails + # These email templates have placeholders in them, and thus must be + # parsed using a templating engine during a request self.email_password_reset_template_html = email_config.get( "password_reset_template_html", "password_reset.html" ) self.email_password_reset_template_text = email_config.get( "password_reset_template_text", "password_reset.txt" ) + self.email_registration_template_html = email_config.get( + "registration_template_html", "registration.html" + ) + self.email_registration_template_text = email_config.get( + "registration_template_text", "registration.txt" + ) self.email_password_reset_template_failure_html = email_config.get( "password_reset_template_failure_html", "password_reset_failure.html" ) - # This template does not support any replaceable variables, so we will - # read it from the disk once during setup + self.email_registration_template_failure_html = email_config.get( + "registration_template_failure_html", "registration_failure.html" + ) + + # These templates do not support any placeholder variables, so we + # will read them from disk once during setup email_password_reset_template_success_html = email_config.get( "password_reset_template_success_html", "password_reset_success.html" ) + email_registration_template_success_html = email_config.get( + "registration_template_success_html", "registration_success.html" + ) # Check templates exist for f in [ self.email_password_reset_template_html, self.email_password_reset_template_text, + self.email_registration_template_html, + self.email_registration_template_text, self.email_password_reset_template_failure_html, email_password_reset_template_success_html, + email_registration_template_success_html, ]: p = os.path.join(self.email_template_dir, f) if not os.path.isfile(p): @@ -177,9 +196,15 @@ def read_config(self, config, **kwargs): filepath = os.path.join( self.email_template_dir, email_password_reset_template_success_html ) - self.email_password_reset_template_success_html_content = self.read_file( + self.email_password_reset_template_success_html = self.read_file( filepath, "email.password_reset_template_success_html" ) + filepath = os.path.join( + self.email_template_dir, email_registration_template_success_html + ) + self.email_registration_template_success_html_content = self.read_file( + filepath, "email.registration_template_success_html" + ) if self.email_enable_notifs: required = [ @@ -291,11 +316,22 @@ def generate_config_section(self, config_dir_path, server_name, **kwargs): # #password_reset_template_html: password_reset.html # #password_reset_template_text: password_reset.txt # + # # Templates for registration emails sent by the homeserver + # # + # #registration_template_html: registration.html + # #registration_template_text: registration.txt + # # # Templates for password reset success and failure pages that a user # # will see after attempting to reset their password # # # #password_reset_template_success_html: password_reset_success.html # #password_reset_template_failure_html: password_reset_failure.html + # + # # Templates for registration success and failure pages that a user + # # will see after attempting to register using an email or phone + # # + # #registration_template_success_html: registration_success.html + # #registration_template_failure_html: registration_failure.html """ diff --git a/synapse/handlers/auth.py b/synapse/handlers/auth.py index 091512aa536d..a59cd4e7f58c 100644 --- a/synapse/handlers/auth.py +++ b/synapse/handlers/auth.py @@ -461,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_threepid_behaviour == ThreepidBehaviour.REMOTE + or self.hs.config.threepid_behaviour == ThreepidBehaviour.REMOTE ): threepid = yield identity_handler.threepid_from_creds(threepid_creds) - elif self.hs.config.email_threepid_behaviour == ThreepidBehaviour.LOCAL: + elif self.hs.config.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 dc34eb707597..dbd86f670cd1 100644 --- a/synapse/handlers/identity.py +++ b/synapse/handlers/identity.py @@ -24,6 +24,7 @@ from twisted.internet import defer from synapse.api.errors import CodeMessageException, HttpResponseException, SynapseError +from synapse.util.stringutils import random_string from ._base import BaseHandler @@ -196,6 +197,84 @@ def try_unbind_threepid_with_id_server(self, mxid, threepid, id_server): return changed + @defer.inlineCallbacks + def send_threepid_validation( + self, + email_address, + client_secret, + send_attempt, + send_email_func, + next_link=None, + ): + """Send a threepid validation email for password reset or + registration purposes + + Args: + email_address (str): The user's email address + client_secret (str): The provided client secret + send_attempt (int): Which send attempt this is + send_email_func (func): A function that takes an email address, token, + client_secret and session_id, sends an email + and returns a Deferred. + next_link (str|None): The URL to redirect the user to after validation + + Returns: + The new session_id upon success + + Raises: + SynapseError is an error occurred when sending the email + """ + # Check that this email/client_secret/send_attempt combo is new or + # greater than what we've seen previously + session = yield self.store.get_threepid_validation_session( + "email", client_secret, address=email_address, validated=False + ) + + # Check to see if a session already exists and that it is not yet + # marked as validated + if session and session.get("validated_at") is None: + session_id = session["session_id"] + last_send_attempt = session["last_send_attempt"] + + # Check that the send_attempt is higher than previous attempts + if send_attempt <= last_send_attempt: + # If not, just return a success without sending an email + return session_id + else: + # An non-validated session does not exist yet. + # Generate a session id + session_id = random_string(16) + + # Generate a new validation token + token = random_string(32) + + # Send the mail with the link containing the token, client_secret + # and session_id + try: + yield send_email_func(email_address, token, client_secret, session_id) + except Exception: + logger.exception( + "Error sending threepid validation email to %s", email_address + ) + raise SynapseError(500, "An error was encountered when sending the email") + + token_expires = ( + self.hs.clock.time_msec() + self.hs.config.email_validation_token_lifetime + ) + + yield self.store.start_or_continue_validation_session( + "email", + email_address, + session_id, + client_secret, + send_attempt, + next_link, + token, + token_expires, + ) + + return session_id + @defer.inlineCallbacks def requestEmailToken( self, id_server, email, client_secret, send_attempt, next_link=None diff --git a/synapse/handlers/register.py b/synapse/handlers/register.py index 4631fab94e39..dce2b7afbfd2 100644 --- a/synapse/handlers/register.py +++ b/synapse/handlers/register.py @@ -414,18 +414,6 @@ def register_email(self, threepidCreds): if not check_3pid_allowed(self.hs, threepid["medium"], threepid["address"]): raise RegistrationError(403, "Third party identifier is not allowed") - @defer.inlineCallbacks - def bind_emails(self, user_id, threepidCreds): - """Links emails with a user ID and informs an identity server. - - Used only by c/s api v1 - """ - - # Now we have a matrix ID, bind it to the threepids we were given - for c in threepidCreds: - # XXX: This should be a deferred list, shouldn't it? - yield self.identity_handler.bind_threepid(c, user_id) - def check_user_id_not_appservice_exclusive(self, user_id, allowed_appservice=None): # don't allow people to register the server notices mxid if self._server_notices_mxid is not None: diff --git a/synapse/push/mailer.py b/synapse/push/mailer.py index 4245ce26f344..72a38a5d65a0 100644 --- a/synapse/push/mailer.py +++ b/synapse/push/mailer.py @@ -131,14 +131,11 @@ def send_password_reset_mail(self, email_address, token, client_secret, sid): email_address (str): Email address we're sending the password reset to token (str): Unique token generated by the server to verify - password reset email was received + the email was received client_secret (str): Unique token generated by the client to group together multiple email sending attempts sid (str): The generated session ID """ - if email.utils.parseaddr(email_address)[1] == "": - raise RuntimeError("Invalid 'to' email address") - link = ( self.hs.config.public_baseurl + "_matrix/client/unstable/password_reset/email/submit_token" @@ -149,7 +146,34 @@ def send_password_reset_mail(self, email_address, token, client_secret, sid): yield self.send_email( email_address, - "[%s] Password Reset Email" % self.hs.config.server_name, + "[%s] Password Reset" % self.hs.config.server_name, + template_vars, + ) + + @defer.inlineCallbacks + def send_registration_mail(self, email_address, token, client_secret, sid): + """Send an email with a registration confirmation link to a user + + Args: + email_address (str): Email address we're sending the registration + link to + token (str): Unique token generated by the server to verify + the email was received + client_secret (str): Unique token generated by the client to + group together multiple email sending attempts + sid (str): The generated session ID + """ + link = ( + self.hs.config.public_baseurl + + "_matrix/client/unstable/registration/email/submit_token" + "?token=%s&client_secret=%s&sid=%s" % (token, client_secret, sid) + ) + + template_vars = {"link": link} + + yield self.send_email( + email_address, + "[%s] Register your Email Address" % self.hs.config.server_name, template_vars, ) diff --git a/synapse/res/templates/password_reset.html b/synapse/res/templates/password_reset.html index 4fa7b367341a..a197bf872cbb 100644 --- a/synapse/res/templates/password_reset.html +++ b/synapse/res/templates/password_reset.html @@ -4,6 +4,6 @@ {{ link }} -

If this was not you, please disregard this email and contact your server administrator. Thank you.

+

If this was not you, do not click the link above and instead contact your server administrator. Thank you.

diff --git a/synapse/res/templates/password_reset.txt b/synapse/res/templates/password_reset.txt index f0deff59a75f..6aa6527560eb 100644 --- a/synapse/res/templates/password_reset.txt +++ b/synapse/res/templates/password_reset.txt @@ -3,5 +3,5 @@ was you, please click the link below to confirm resetting your password: {{ link }} -If this was not you, please disregard this email and contact your server -administrator. Thank you. +If this was not you, DO NOT click the link above and instead contact your +server administrator. Thank you. diff --git a/synapse/res/templates/password_reset_failure.html b/synapse/res/templates/password_reset_failure.html index 0b132cf8db94..9e3c4446e315 100644 --- a/synapse/res/templates/password_reset_failure.html +++ b/synapse/res/templates/password_reset_failure.html @@ -1,6 +1,8 @@ -

{{ failure_reason }}. Your password has not been reset.

+

The request failed for the following reason: {{ failure_reason }}.

+ +

Your password has not been reset.

diff --git a/synapse/res/templates/registration.html b/synapse/res/templates/registration.html new file mode 100644 index 000000000000..16730a527fce --- /dev/null +++ b/synapse/res/templates/registration.html @@ -0,0 +1,11 @@ + + +

You have asked us to register this email with a new Matrix account. If this was you, please click the link below to confirm your email address:

+ + Verify Your Email Address + +

If this was not you, you can safely disregard this email.

+ +

Thank you.

+ + diff --git a/synapse/res/templates/registration.txt b/synapse/res/templates/registration.txt new file mode 100644 index 000000000000..cb4f16a90ca1 --- /dev/null +++ b/synapse/res/templates/registration.txt @@ -0,0 +1,10 @@ +Hello there, + +You have asked us to register this email with a new Matrix account. If this +was you, please click the link below to confirm your email address: + +{{ link }} + +If this was not you, you can safely disregard this email. + +Thank you. diff --git a/synapse/res/templates/registration_failure.html b/synapse/res/templates/registration_failure.html new file mode 100644 index 000000000000..2833d79c3738 --- /dev/null +++ b/synapse/res/templates/registration_failure.html @@ -0,0 +1,6 @@ + + + +

Validation failed for the following reason: {{ failure_reason }}.

+ + diff --git a/synapse/res/templates/registration_success.html b/synapse/res/templates/registration_success.html new file mode 100644 index 000000000000..fbd6e4018f7d --- /dev/null +++ b/synapse/res/templates/registration_success.html @@ -0,0 +1,6 @@ + + + +

Your email has now been validated, please return to your client. You may now close this window.

+ + diff --git a/synapse/rest/client/v2_alpha/_base.py b/synapse/rest/client/v2_alpha/_base.py index e3d59ac3ac5e..8250ae0ae116 100644 --- a/synapse/rest/client/v2_alpha/_base.py +++ b/synapse/rest/client/v2_alpha/_base.py @@ -37,6 +37,7 @@ def client_patterns(path_regex, releases=(0,), unstable=True, v1=False): SRE_Pattern """ patterns = [] + if unstable: unstable_prefix = CLIENT_API_PREFIX + "/unstable" patterns.append(re.compile("^" + unstable_prefix + path_regex)) @@ -46,6 +47,7 @@ def client_patterns(path_regex, releases=(0,), unstable=True, v1=False): for release in releases: new_prefix = CLIENT_API_PREFIX + "/r%d" % (release,) patterns.append(re.compile("^" + new_prefix + path_regex)) + return patterns diff --git a/synapse/rest/client/v2_alpha/account.py b/synapse/rest/client/v2_alpha/account.py index 2c649259a20b..552ba7cc621a 100644 --- a/synapse/rest/client/v2_alpha/account.py +++ b/synapse/rest/client/v2_alpha/account.py @@ -33,7 +33,6 @@ parse_string, ) from synapse.util.msisdn import phone_number_to_msisdn -from synapse.util.stringutils import random_string from synapse.util.threepids import check_3pid_allowed from ._base import client_patterns, interactive_auth_handler @@ -51,7 +50,7 @@ def __init__(self, hs): self.config = hs.config self.identity_handler = hs.get_handlers().identity_handler - if self.config.email_threepid_behaviour == ThreepidBehaviour.LOCAL: + if self.config.threepid_behaviour == ThreepidBehaviour.LOCAL: from synapse.push.mailer import Mailer, load_jinja2_templates templates = load_jinja2_templates( @@ -68,8 +67,8 @@ def __init__(self, hs): @defer.inlineCallbacks def on_POST(self, request): - if self.config.email_threepid_behaviour == ThreepidBehaviour.OFF: - if self.config.local_threepid_emails_disabled_due_to_config: + if self.config.threepid_behaviour == ThreepidBehaviour.OFF: + if self.config.local_threepid_handling_disabled_due_to_email_config: logger.warn( "User password resets have been disabled due to lack of email config" ) @@ -101,7 +100,7 @@ def on_POST(self, request): if existing_user_id is None: raise SynapseError(400, "Email not found", Codes.THREEPID_NOT_FOUND) - if self.config.email_threepid_behaviour == ThreepidBehaviour.REMOTE: + if self.config.threepid_behaviour == ThreepidBehaviour.REMOTE: # Have the configured identity server handle the request if not self.hs.config.account_threepid_delegate: logger.warn( @@ -119,90 +118,20 @@ def on_POST(self, request): send_attempt, next_link, ) - elif self.config.email_threepid_behaviour == ThreepidBehaviour.LOCAL: + else: # Send password reset emails from Synapse - sid = yield self.send_password_reset( - email, client_secret, send_attempt, next_link + sid = yield self.identity_handler.send_threepid_validation( + email, + client_secret, + send_attempt, + self.mailer.send_password_reset_mail, + next_link, ) # 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) - - @defer.inlineCallbacks - def send_password_reset(self, email, client_secret, send_attempt, next_link=None): - """Send a password reset email - - Args: - 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 - - Raises: - SynapseError is an error occurred when sending the email - """ - # Check that this email/client_secret/send_attempt combo is new or - # greater than what we've seen previously - session = yield self.datastore.get_threepid_validation_session( - "email", client_secret, address=email, validated=False - ) - - # Check to see if a session already exists and that it is not yet - # marked as validated - if session and session.get("validated_at") is None: - session_id = session["session_id"] - last_send_attempt = session["last_send_attempt"] - - # Check that the send_attempt is higher than previous attempts - if send_attempt <= last_send_attempt: - # If not, just return a success without sending an email - return session_id - else: - # An non-validated session does not exist yet. - # Generate a session id - session_id = random_string(16) - - # Generate a new validation token - token = random_string(32) - - # Send the mail with the link containing the token, client_secret - # and session_id - try: - yield self.mailer.send_password_reset_mail( - email, token, client_secret, session_id - ) - except Exception: - logger.exception("Error sending a password reset email to %s", email) - raise SynapseError( - 500, "An error was encountered when sending the password reset email" - ) - - token_expires = ( - self.hs.clock.time_msec() + self.config.email_validation_token_lifetime - ) - - yield self.datastore.start_or_continue_validation_session( - "email", - email, - session_id, - client_secret, - send_attempt, - next_link, - token, - token_expires, - ) - - return session_id + return 200, ret class MsisdnPasswordRequestTokenRestServlet(RestServlet): @@ -243,7 +172,7 @@ def on_POST(self, request): if existing_user_id is None: raise SynapseError(400, "MSISDN not found", Codes.THREEPID_NOT_FOUND) - if self.config.email_threepid_behaviour == ThreepidBehaviour.REMOTE: + if self.config.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 " @@ -286,7 +215,7 @@ def __init__(self, hs): self.auth = hs.get_auth() self.config = hs.config self.clock = hs.get_clock() - self.datastore = hs.get_datastore() + self.store = hs.get_datastore() @defer.inlineCallbacks def on_GET(self, request, medium): @@ -294,23 +223,23 @@ def on_GET(self, request, medium): raise SynapseError( 400, "This medium is currently not supported for password resets" ) - if self.config.email_threepid_behaviour == ThreepidBehaviour.OFF: - if self.config.local_threepid_emails_disabled_due_to_config: + if self.config.threepid_behaviour == ThreepidBehaviour.OFF: + if self.config.local_threepid_handling_disabled_due_to_email_config: logger.warn( - "User password resets have been disabled due to lack of email config" + "Password reset emails have been disabled due to lack of an email config" ) raise SynapseError( - 400, "Email-based password resets have been disabled on this server" + 400, "Email-based password resets are disabled on this server" ) - sid = parse_string(request, "sid") - client_secret = parse_string(request, "client_secret") - token = parse_string(request, "token") + sid = parse_string(request, "sid", required=True) + client_secret = parse_string(request, "client_secret", required=True) + token = parse_string(request, "token", required=True) - # Attempt to validate a 3PID sesssion + # Attempt to validate a 3PID session try: # Mark the session as valid - next_link = yield self.datastore.validate_threepid_session( + next_link = yield self.store.validate_threepid_session( sid, client_secret, token, self.clock.time_msec() ) @@ -327,7 +256,7 @@ def on_GET(self, request, medium): return None # Otherwise show the success template - html = self.config.email_password_reset_template_success_html_content + html = self.config.email_password_reset_template_success_html request.setResponseCode(200) except ThreepidValidationError as e: # Show a failure page with a reason @@ -340,7 +269,6 @@ def on_GET(self, request, medium): request.write(html.encode("utf-8")) finish_request(request) - return None def load_jinja2_template(self, template_dir, template_filename, template_vars): """Loads a jinja2 template with variables to insert @@ -499,8 +427,9 @@ class EmailThreepidRequestTokenRestServlet(RestServlet): PATTERNS = client_patterns("/account/3pid/email/requestToken$") def __init__(self, hs): - self.hs = hs super(EmailThreepidRequestTokenRestServlet, self).__init__() + self.hs = hs + self.config = hs.config self.identity_handler = hs.get_handlers().identity_handler self.store = self.hs.get_datastore() @@ -516,7 +445,7 @@ def on_POST(self, request): send_attempt = body["send_attempt"] next_link = body.get("next_link") # Optional param - if not check_3pid_allowed(self.hs, "email", body["email"]): + if not check_3pid_allowed(self.hs, "email", email): raise SynapseError( 403, "Your email domain is not authorized on this server", @@ -533,7 +462,7 @@ def on_POST(self, request): ret = yield self.identity_handler.requestEmailToken( id_server, email, client_secret, send_attempt, next_link ) - return (200, ret) + return 200, ret class MsisdnThreepidRequestTokenRestServlet(RestServlet): @@ -542,8 +471,8 @@ class MsisdnThreepidRequestTokenRestServlet(RestServlet): def __init__(self, hs): self.hs = hs super(MsisdnThreepidRequestTokenRestServlet, self).__init__() - self.identity_handler = hs.get_handlers().identity_handler self.store = self.hs.get_datastore() + self.identity_handler = hs.get_handlers().identity_handler @defer.inlineCallbacks def on_POST(self, request): diff --git a/synapse/rest/client/v2_alpha/register.py b/synapse/rest/client/v2_alpha/register.py index ecafee5ae7bb..a5d560516e4e 100644 --- a/synapse/rest/client/v2_alpha/register.py +++ b/synapse/rest/client/v2_alpha/register.py @@ -28,11 +28,13 @@ Codes, LimitExceededError, SynapseError, + ThreepidValidationError, UnrecognizedRequestError, ) from synapse.config.emailconfig import ThreepidBehaviour from synapse.config.ratelimiting import FederationRateLimitConfig from synapse.config.server import is_threepid_reserved +from synapse.http.server import finish_request from synapse.http.servlet import ( RestServlet, assert_params_in_dict, @@ -71,9 +73,33 @@ def __init__(self, hs): super(EmailRegisterRequestTokenRestServlet, self).__init__() self.hs = hs self.identity_handler = hs.get_handlers().identity_handler + self.config = hs.config + + if self.hs.config.threepid_behaviour == ThreepidBehaviour.LOCAL: + from synapse.push.mailer import Mailer, load_jinja2_templates + + templates = load_jinja2_templates( + config=hs.config, + template_html_name=hs.config.email_registration_template_html, + template_text_name=hs.config.email_registration_template_text, + ) + self.mailer = Mailer( + hs=self.hs, + app_name=self.hs.config.email_app_name, + template_html=templates[0], + template_text=templates[1], + ) @defer.inlineCallbacks def on_POST(self, request): + if self.hs.config.threepid_behaviour == ThreepidBehaviour.OFF: + if self.hs.config.local_threepid_handling_disabled_due_to_email_config: + logger.warn( + "Email registration has been disabled due to lack of email config" + ) + raise SynapseError( + 400, "Email-based registration has been disabled on this server" + ) body = parse_json_object_from_request(request) assert_params_in_dict(body, ["client_secret", "email", "send_attempt"]) @@ -84,7 +110,7 @@ def on_POST(self, request): send_attempt = body["send_attempt"] next_link = body.get("next_link") # Optional param - if not check_3pid_allowed(self.hs, "email", body["email"]): + if not check_3pid_allowed(self.hs, "email", email): raise SynapseError( 403, "Your email domain is not authorized to register on this server", @@ -98,24 +124,37 @@ def on_POST(self, request): if existing_user_id is not None: raise SynapseError(400, "Email is already in use", Codes.THREEPID_IN_USE) - if not self.hs.config.account_threepid_delegate: - logger.warn( - "No upstream account_threepid_delegate configured on the server to handle " - "this request" + if self.config.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 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, ) - raise SynapseError( - 400, "Registration by email is not supported on this homeserver" + else: + # Send registration emails from Synapse + sid = yield self.identity_handler.send_threepid_validation( + email, + client_secret, + send_attempt, + self.mailer.send_registration_mail, + next_link, ) - ret = yield self.identity_handler.requestEmailToken( - self.hs.config.account_threepid_delegate, - email, - client_secret, - send_attempt, - next_link, - ) + # Wrap the session id in a JSON object + ret = {"sid": sid} - return (200, ret) + return 200, ret class MsisdnRegisterRequestTokenRestServlet(RestServlet): @@ -161,7 +200,7 @@ def on_POST(self, request): 400, "Phone number is already in use", Codes.THREEPID_IN_USE ) - if self.config.email_threepid_behaviour == ThreepidBehaviour.REMOTE: + if self.config.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 " @@ -187,6 +226,81 @@ def on_POST(self, request): ) +class RegistrationSubmitTokenServlet(RestServlet): + """Handles registration 3PID validation token submission""" + + PATTERNS = client_patterns( + "/registration/(?P[^/]*)/submit_token$", releases=(), unstable=True + ) + + def __init__(self, hs): + """ + Args: + hs (synapse.server.HomeServer): server + """ + super(RegistrationSubmitTokenServlet, self).__init__() + self.hs = hs + self.auth = hs.get_auth() + self.config = hs.config + self.clock = hs.get_clock() + self.store = hs.get_datastore() + + @defer.inlineCallbacks + def on_GET(self, request, medium): + if medium != "email": + raise SynapseError( + 400, "This medium is currently not supported for registration" + ) + if self.config.threepid_behaviour == ThreepidBehaviour.OFF: + if self.config.local_threepid_handling_disabled_due_to_email_config: + logger.warn( + "User registration via email has been disabled due to lack of email config" + ) + raise SynapseError( + 400, "Email-based registration is disabled on this server" + ) + + sid = parse_string(request, "sid", required=True) + client_secret = parse_string(request, "client_secret", required=True) + token = parse_string(request, "token", required=True) + + # Attempt to validate a 3PID session + try: + # Mark the session as valid + next_link = yield self.store.validate_threepid_session( + sid, client_secret, token, self.clock.time_msec() + ) + + # Perform a 302 redirect if next_link is set + if next_link: + if next_link.startswith("file:///"): + logger.warn( + "Not redirecting to next_link as it is a local file: address" + ) + else: + request.setResponseCode(302) + request.setHeader("Location", next_link) + finish_request(request) + return None + + # Otherwise show the success template + html = self.config.email_registration_template_success_html_content + + request.setResponseCode(200) + except ThreepidValidationError as e: + # Show a failure page with a reason + html = self.load_jinja2_template( + self.config.email_template_dir, + self.config.email_registration_template_failure_html, + template_vars={"failure_reason": e.msg}, + ) + request.setResponseCode(e.code) + + request.write(html.encode("utf-8")) + finish_request(request) + return None + + class UsernameAvailabilityRestServlet(RestServlet): PATTERNS = client_patterns("/register/available") @@ -601,4 +715,5 @@ def register_servlets(hs, http_server): EmailRegisterRequestTokenRestServlet(hs).register(http_server) MsisdnRegisterRequestTokenRestServlet(hs).register(http_server) UsernameAvailabilityRestServlet(hs).register(http_server) + RegistrationSubmitTokenServlet(hs).register(http_server) RegisterRestServlet(hs).register(http_server)