summary refs log tree commit diff
path: root/synapse/rest
diff options
context:
space:
mode:
authorAndrew Morgan <andrew@amorgan.xyz>2019-05-24 15:38:51 +0100
committerAndrew Morgan <andrew@amorgan.xyz>2019-06-04 18:49:54 +0100
commitdbdebc2c6f8d74928425c4d52d5e6426660bd65e (patch)
treecb3270782431dd7f8c5ede4857333ac04d49e94b /synapse/rest
parentFix appservice timestamp massaging (#5233) (diff)
downloadsynapse-dbdebc2c6f8d74928425c4d52d5e6426660bd65e.tar.xz
Ability to send password reset emails
This changes the default behaviour of Synapse to send password reset
emails itself rather than through an identity server. The reasoning
behind the change is to prevent a malicious identity server from
being able to initiate a password reset attempt and then answering
it, successfully resetting their password, all without the user's
knowledge. This also aides in decentralisation by putting less
trust on the identity server itself, which traditionally is quite
centralised.

If users wish to continue with the old behaviour of proxying
password reset requests through the user's configured identity
server, they can do so by setting
email.enable_password_reset_from_is to True in Synapse's config.

Users should be able that with that option disabled (the default),
password resets will now no longer work unless email sending has
been enabled and set up correctly.
Diffstat (limited to 'synapse/rest')
-rw-r--r--synapse/rest/client/v2_alpha/account.py109
1 files changed, 107 insertions, 2 deletions
diff --git a/synapse/rest/client/v2_alpha/account.py b/synapse/rest/client/v2_alpha/account.py
index ee069179f0..37d5e67748 100644
--- a/synapse/rest/client/v2_alpha/account.py
+++ b/synapse/rest/client/v2_alpha/account.py
@@ -27,7 +27,9 @@ from synapse.http.servlet import (
     assert_params_in_dict,
     parse_json_object_from_request,
 )
+from synapse.push.mailer import Mailer, load_jinja2_templates
 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_v2_patterns, interactive_auth_handler
@@ -41,14 +43,35 @@ class EmailPasswordRequestTokenRestServlet(RestServlet):
     def __init__(self, hs):
         super(EmailPasswordRequestTokenRestServlet, self).__init__()
         self.hs = hs
+        self.datastore = hs.get_datastore()
+        self.config = hs.config
         self.identity_handler = hs.get_handlers().identity_handler
 
+        if not hs.config.email_enable_password_reset_from_is:
+            templates = load_jinja2_templates(
+                config=hs.config,
+                template_html_name=hs.config.email_password_reset_template_html,
+                template_text_name=hs.config.email_password_reset_template_text,
+            )
+            self.mailer = Mailer(
+                hs=self.hs,
+                app_name=self.config.email_app_name,
+                template_html=templates[0],
+                template_text=templates[1],
+            )
+
+        # Create a background job for culling expired 3PID validity tokens
+        # every minute
+        hs.get_clock().looping_call(
+            self.datastore.cull_expired_threepid_validation_tokens, 60 * 1000,
+        )
+
     @defer.inlineCallbacks
     def on_POST(self, request):
         body = parse_json_object_from_request(request)
 
         assert_params_in_dict(body, [
-            'id_server', 'client_secret', 'email', 'send_attempt'
+            'client_secret', 'email', 'send_attempt'
         ])
 
         if not check_3pid_allowed(self.hs, "email", body['email']):
@@ -65,9 +88,90 @@ class EmailPasswordRequestTokenRestServlet(RestServlet):
         if existingUid is None:
             raise SynapseError(400, "Email not found", Codes.THREEPID_NOT_FOUND)
 
-        ret = yield self.identity_handler.requestEmailToken(**body)
+        if self.config.email_enable_password_reset_from_is:
+            if 'id_server' not in body:
+                raise SynapseError(400, "Missing 'id_server' param in body")
+
+            # Have the identity server handle the password reset flow
+            ret = yield self.identity_handler.requestEmailToken(**body)
+        else:
+            # Send password reset emails from Synapse
+            sid = yield self.send_password_reset(**body)
+
+            # Wrap the session id in a JSON object
+            ret = {"sid": sid}
+
         defer.returnValue((200, ret))
 
+    @defer.inlineCallbacks
+    def send_password_reset(self, email, client_secret, send_attempt, **kwargs):
+        """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
+
+        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
+        ret = yield self.datastore.get_threepid_validation_session(
+            "email", client_secret, address=email, validated=False,
+        )
+
+        logger.info("Ret is %s", ret)
+
+        # Check to see if a session already exists and that it is not yet
+        # marked as validated
+        if ret and ret.get("validated_at") is None:
+            session_id = ret['session_id']
+            last_send_attempt = ret['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
+                defer.returnValue(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.validation_token_lifetime * 1000)
+
+        yield self.datastore.insert_threepid_validation_token(
+            session_id, token, kwargs.get("next_link"), token_expires,
+        )
+
+        # Save the session_id and send_attempt to the database
+        yield self.datastore.upsert_threepid_validation_session(
+            "email", email, client_secret, send_attempt, session_id,
+        )
+
+        defer.returnValue(session_id)
+
 
 class MsisdnPasswordRequestTokenRestServlet(RestServlet):
     PATTERNS = client_v2_patterns("/account/password/msisdn/requestToken$")
@@ -144,6 +248,7 @@ class PasswordRestServlet(RestServlet):
             result, params, _ = yield self.auth_handler.check_auth(
                 [[LoginType.EMAIL_IDENTITY], [LoginType.MSISDN]],
                 body, self.hs.get_ip_from_request(request),
+                password_servlet=True,
             )
 
             if LoginType.EMAIL_IDENTITY in result: