From 75f7a7b7bb30f3a915bc396a2e7fa4ac58805975 Mon Sep 17 00:00:00 2001 From: Andrew Morgan <1342360+anoadragon453@users.noreply.github.com> Date: Fri, 30 Aug 2019 11:00:32 +0100 Subject: [PATCH] [1/2] Allow homeservers to send registration emails | Sending the email (#5835) ~~Fixes https://github.com/matrix-org/synapse/issues/5833~~ Moved out to ~~https://github.com/matrix-org/synapse/pull/5863~~ Part of fixing https://github.com/matrix-org/synapse/issues/5751 Decouples the activity of sending registration emails and binding them to an identity server. This PR simply sends the registration email, but clicking it does not approve the user for registration. That will come in PR #2. Some of this makes use of existing stuff for sending password reset emails from Synapse. Some work was done to make that stuff even more generic. Upgrade notes: * There is a new top-level config option, `account_threepid_delegate` which defines the address of an identity server that you would like to send registration/password reset emails on your behalf. The option `email.trust_identity_server_for_password_resets` has been replaced by this. If you set `email.trust_identity_server_for_password_resets` in your config to `true`, please remove it and configure `account_threepid_delegate` instead. The [sample config](https://github.com/matrix-org/synapse/blob/anoa/reg_email_sending_email/docs/sample_config.yaml) has information on how to configure it. Note: This PR does not allow homeservers to send emails when simply adding an email to your account. That will come after this and will be blocked on a new MSC. Part [2/2] will be successfully completing the registration step when `/submit_token` is hit with the correct details, and clearing out the `password_servlet` flag stuff, which is no longer needed. Will be a much smaller PR than this one. ~~Requires https://github.com/matrix-org/synapse/pull/5863~~ has been merged into the base branch. ~~Requires https://github.com/matrix-org/synapse/pull/5876~~ has been merged into the base branch. --- UPGRADE.rst | 62 ++++++++ changelog.d/5835.feature | 1 + docs/sample_config.yaml | 11 ++ synapse/config/emailconfig.py | 66 ++++++-- synapse/handlers/auth.py | 4 +- synapse/handlers/identity.py | 79 ++++++++++ synapse/handlers/register.py | 12 -- synapse/push/mailer.py | 34 +++- synapse/res/templates/password_reset.html | 2 +- synapse/res/templates/password_reset.txt | 4 +- .../res/templates/password_reset_failure.html | 4 +- synapse/res/templates/registration.html | 11 ++ synapse/res/templates/registration.txt | 10 ++ .../res/templates/registration_failure.html | 6 + .../res/templates/registration_success.html | 6 + synapse/rest/client/v2_alpha/_base.py | 2 + synapse/rest/client/v2_alpha/account.py | 129 ++++----------- synapse/rest/client/v2_alpha/register.py | 147 ++++++++++++++++-- 18 files changed, 436 insertions(+), 154 deletions(-) create mode 100644 changelog.d/5835.feature create mode 100644 synapse/res/templates/registration.html create mode 100644 synapse/res/templates/registration.txt create mode 100644 synapse/res/templates/registration_failure.html create mode 100644 synapse/res/templates/registration_success.html 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)