diff --git a/changelog.d/5424.misc b/changelog.d/5424.misc
new file mode 100644
index 000000000..b92b50e31
--- /dev/null
+++ b/changelog.d/5424.misc
@@ -0,0 +1 @@
+Move password reset links to /_matrix/client/unstable namespace.
diff --git a/synapse/app/homeserver.py b/synapse/app/homeserver.py
index df524a23d..1045d2894 100755
--- a/synapse/app/homeserver.py
+++ b/synapse/app/homeserver.py
@@ -176,7 +176,6 @@ class SynapseHomeServer(HomeServer):
resources.update({
"/_matrix/client/api/v1": client_resource,
- "/_synapse/password_reset": client_resource,
"/_matrix/client/r0": client_resource,
"/_matrix/client/unstable": client_resource,
"/_matrix/client/v2_alpha": client_resource,
diff --git a/synapse/push/mailer.py b/synapse/push/mailer.py
index 4bc9eb731..099f9545a 100644
--- a/synapse/push/mailer.py
+++ b/synapse/push/mailer.py
@@ -117,7 +117,7 @@ class Mailer(object):
link = (
self.hs.config.public_baseurl +
- "_synapse/password_reset/email/submit_token"
+ "_matrix/client/unstable/password_reset/email/submit_token"
"?token=%s&client_secret=%s&sid=%s" %
(token, client_secret, sid)
)
diff --git a/synapse/python_dependencies.py b/synapse/python_dependencies.py
index 6efd81f20..7dfa78dad 100644
--- a/synapse/python_dependencies.py
+++ b/synapse/python_dependencies.py
@@ -80,7 +80,7 @@ REQUIREMENTS = [
]
CONDITIONAL_REQUIREMENTS = {
- "email": ["Jinja2>=2.9", "bleach>=1.4.2"],
+ "email": ["Jinja2>=2.9", "bleach>=1.4.3"],
"matrix-synapse-ldap3": ["matrix-synapse-ldap3>=0.1"],
# we use execute_batch, which arrived in psycopg 2.7.
diff --git a/synapse/res/templates/password_reset_success.html b/synapse/res/templates/password_reset_success.html
index 7b6fa5e6f..7324d66d1 100644
--- a/synapse/res/templates/password_reset_success.html
+++ b/synapse/res/templates/password_reset_success.html
@@ -1,6 +1,6 @@
-Your password was successfully reset. You may now close this window.
+Your email has now been validated, please return to your client to reset your password. You may now close this window.
diff --git a/synapse/rest/client/v2_alpha/account.py b/synapse/rest/client/v2_alpha/account.py
index e4c63b69b..7025f486e 100644
--- a/synapse/rest/client/v2_alpha/account.py
+++ b/synapse/rest/client/v2_alpha/account.py
@@ -15,7 +15,6 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import logging
-import re
from six.moves import http_client
@@ -228,9 +227,11 @@ class MsisdnPasswordRequestTokenRestServlet(RestServlet):
class PasswordResetSubmitTokenServlet(RestServlet):
"""Handles 3PID validation token submission"""
- PATTERNS = [
- re.compile("^/_synapse/password_reset/(?P[^/]*)/submit_token/*$"),
- ]
+ PATTERNS = client_patterns(
+ "/password_reset/(?P[^/]*)/submit_token/*$",
+ releases=(),
+ unstable=True,
+ )
def __init__(self, hs):
"""
diff --git a/tests/push/test_email.py b/tests/push/test_email.py
index 9cdde1a9b..9bc5f07de 100644
--- a/tests/push/test_email.py
+++ b/tests/push/test_email.py
@@ -24,15 +24,9 @@ from synapse.rest.client.v1 import login, room
from tests.unittest import HomeserverTestCase
-try:
- from synapse.push.mailer import load_jinja2_templates
-except Exception:
- load_jinja2_templates = None
-
class EmailPusherTests(HomeserverTestCase):
- skip = "No Jinja installed" if not load_jinja2_templates else None
servlets = [
synapse.rest.admin.register_servlets_for_client_rest_resource,
room.register_servlets,
diff --git a/tests/push/test_http.py b/tests/push/test_http.py
index aba618b2b..22c3f73ef 100644
--- a/tests/push/test_http.py
+++ b/tests/push/test_http.py
@@ -23,15 +23,9 @@ from synapse.util.logcontext import make_deferred_yieldable
from tests.unittest import HomeserverTestCase
-try:
- from synapse.push.mailer import load_jinja2_templates
-except Exception:
- load_jinja2_templates = None
-
class HTTPPusherTests(HomeserverTestCase):
- skip = "No Jinja installed" if not load_jinja2_templates else None
servlets = [
synapse.rest.admin.register_servlets_for_client_rest_resource,
room.register_servlets,
diff --git a/tests/rest/client/test_consent.py b/tests/rest/client/test_consent.py
index 88f8f1abd..efc5a99db 100644
--- a/tests/rest/client/test_consent.py
+++ b/tests/rest/client/test_consent.py
@@ -23,14 +23,8 @@ from synapse.rest.consent import consent_resource
from tests import unittest
from tests.server import render
-try:
- from synapse.push.mailer import load_jinja2_templates
-except Exception:
- load_jinja2_templates = None
-
class ConsentResourceTestCase(unittest.HomeserverTestCase):
- skip = "No Jinja installed" if not load_jinja2_templates else None
servlets = [
synapse.rest.admin.register_servlets_for_client_rest_resource,
room.register_servlets,
diff --git a/tests/rest/client/v2_alpha/test_account.py b/tests/rest/client/v2_alpha/test_account.py
new file mode 100644
index 000000000..0d1c0868c
--- /dev/null
+++ b/tests/rest/client/v2_alpha/test_account.py
@@ -0,0 +1,241 @@
+# -*- coding: utf-8 -*-
+# Copyright 2015-2016 OpenMarket Ltd
+# Copyright 2017-2018 New Vector Ltd
+# Copyright 2019 The Matrix.org Foundation C.I.C.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import os
+import re
+from email.parser import Parser
+
+import pkg_resources
+
+import synapse.rest.admin
+from synapse.api.constants import LoginType
+from synapse.rest.client.v1 import login
+from synapse.rest.client.v2_alpha import account, register
+
+from tests import unittest
+
+
+class PasswordResetTestCase(unittest.HomeserverTestCase):
+
+ servlets = [
+ account.register_servlets,
+ synapse.rest.admin.register_servlets_for_client_rest_resource,
+ register.register_servlets,
+ login.register_servlets,
+ ]
+
+ def make_homeserver(self, reactor, clock):
+ config = self.default_config()
+
+ # Email config.
+ self.email_attempts = []
+
+ def sendmail(smtphost, from_addr, to_addrs, msg, **kwargs):
+ self.email_attempts.append(msg)
+ return
+
+ config["email"] = {
+ "enable_notifs": False,
+ "template_dir": os.path.abspath(
+ pkg_resources.resource_filename("synapse", "res/templates")
+ ),
+ "smtp_host": "127.0.0.1",
+ "smtp_port": 20,
+ "require_transport_security": False,
+ "smtp_user": None,
+ "smtp_pass": None,
+ "notif_from": "test@example.com",
+ }
+ config["public_baseurl"] = "https://example.com"
+
+ hs = self.setup_test_homeserver(config=config, sendmail=sendmail)
+ return hs
+
+ def prepare(self, reactor, clock, hs):
+ self.store = hs.get_datastore()
+
+ def test_basic_password_reset(self):
+ """Test basic password reset flow
+ """
+ old_password = "monkey"
+ new_password = "kangeroo"
+
+ user_id = self.register_user("kermit", old_password)
+ self.login("kermit", old_password)
+
+ email = "test@example.com"
+
+ # Add a threepid
+ self.get_success(
+ self.store.user_add_threepid(
+ user_id=user_id,
+ medium="email",
+ address=email,
+ validated_at=0,
+ added_at=0,
+ )
+ )
+
+ client_secret = "foobar"
+ session_id = self._request_token(email, client_secret)
+
+ self.assertEquals(len(self.email_attempts), 1)
+ link = self._get_link_from_email()
+
+ self._validate_token(link)
+
+ self._reset_password(new_password, session_id, client_secret)
+
+ # Assert we can log in with the new password
+ self.login("kermit", new_password)
+
+ # Assert we can't log in with the old password
+ self.attempt_wrong_password_login("kermit", old_password)
+
+ def test_cant_reset_password_without_clicking_link(self):
+ """Test that we do actually need to click the link in the email
+ """
+ old_password = "monkey"
+ new_password = "kangeroo"
+
+ user_id = self.register_user("kermit", old_password)
+ self.login("kermit", old_password)
+
+ email = "test@example.com"
+
+ # Add a threepid
+ self.get_success(
+ self.store.user_add_threepid(
+ user_id=user_id,
+ medium="email",
+ address=email,
+ validated_at=0,
+ added_at=0,
+ )
+ )
+
+ client_secret = "foobar"
+ session_id = self._request_token(email, client_secret)
+
+ self.assertEquals(len(self.email_attempts), 1)
+
+ # Attempt to reset password without clicking the link
+ self._reset_password(
+ new_password, session_id, client_secret, expected_code=401,
+ )
+
+ # Assert we can log in with the old password
+ self.login("kermit", old_password)
+
+ # Assert we can't log in with the new password
+ self.attempt_wrong_password_login("kermit", new_password)
+
+ def test_no_valid_token(self):
+ """Test that we do actually need to request a token and can't just
+ make a session up.
+ """
+ old_password = "monkey"
+ new_password = "kangeroo"
+
+ user_id = self.register_user("kermit", old_password)
+ self.login("kermit", old_password)
+
+ email = "test@example.com"
+
+ # Add a threepid
+ self.get_success(
+ self.store.user_add_threepid(
+ user_id=user_id,
+ medium="email",
+ address=email,
+ validated_at=0,
+ added_at=0,
+ )
+ )
+
+ client_secret = "foobar"
+ session_id = "weasle"
+
+ # Attempt to reset password without even requesting an email
+ self._reset_password(
+ new_password, session_id, client_secret, expected_code=401,
+ )
+
+ # Assert we can log in with the old password
+ self.login("kermit", old_password)
+
+ # Assert we can't log in with the new password
+ self.attempt_wrong_password_login("kermit", new_password)
+
+ def _request_token(self, email, client_secret):
+ request, channel = self.make_request(
+ "POST",
+ b"account/password/email/requestToken",
+ {"client_secret": client_secret, "email": email, "send_attempt": 1},
+ )
+ self.render(request)
+ self.assertEquals(200, channel.code, channel.result)
+
+ return channel.json_body["sid"]
+
+ def _validate_token(self, link):
+ # Remove the host
+ path = link.replace("https://example.com", "")
+
+ request, channel = self.make_request("GET", path, shorthand=False)
+ self.render(request)
+ self.assertEquals(200, channel.code, channel.result)
+
+ def _get_link_from_email(self):
+ assert self.email_attempts, "No emails have been sent"
+
+ raw_msg = self.email_attempts[-1].decode("UTF-8")
+ mail = Parser().parsestr(raw_msg)
+
+ text = None
+ for part in mail.walk():
+ if part.get_content_type() == "text/plain":
+ text = part.get_payload(decode=True).decode("UTF-8")
+ break
+
+ if not text:
+ self.fail("Could not find text portion of email to parse")
+
+ match = re.search(r"https://example.com\S+", text)
+ assert match, "Could not find link in email"
+
+ return match.group(0)
+
+ def _reset_password(
+ self, new_password, session_id, client_secret, expected_code=200
+ ):
+ request, channel = self.make_request(
+ "POST",
+ b"account/password",
+ {
+ "new_password": new_password,
+ "auth": {
+ "type": LoginType.EMAIL_IDENTITY,
+ "threepid_creds": {
+ "client_secret": client_secret,
+ "sid": session_id,
+ },
+ },
+ },
+ )
+ self.render(request)
+ self.assertEquals(expected_code, channel.code, channel.result)
diff --git a/tests/rest/client/v2_alpha/test_register.py b/tests/rest/client/v2_alpha/test_register.py
index 0cb6a363d..e9d8f3c73 100644
--- a/tests/rest/client/v2_alpha/test_register.py
+++ b/tests/rest/client/v2_alpha/test_register.py
@@ -30,11 +30,6 @@ from synapse.rest.client.v2_alpha import account_validity, register, sync
from tests import unittest
-try:
- from synapse.push.mailer import load_jinja2_templates
-except ImportError:
- load_jinja2_templates = None
-
class RegisterRestServletTestCase(unittest.HomeserverTestCase):
@@ -307,7 +302,6 @@ class AccountValidityTestCase(unittest.HomeserverTestCase):
class AccountValidityRenewalByEmailTestCase(unittest.HomeserverTestCase):
- skip = "No Jinja installed" if not load_jinja2_templates else None
servlets = [
register.register_servlets,
synapse.rest.admin.register_servlets_for_client_rest_resource,
diff --git a/tests/unittest.py b/tests/unittest.py
index 26204470b..7dbb64af5 100644
--- a/tests/unittest.py
+++ b/tests/unittest.py
@@ -441,3 +441,15 @@ class HomeserverTestCase(TestCase):
access_token = channel.json_body["access_token"]
return access_token
+
+ def attempt_wrong_password_login(self, username, password):
+ """Attempts to login as the user with the given password, asserting
+ that the attempt *fails*.
+ """
+ body = {"type": "m.login.password", "user": username, "password": password}
+
+ request, channel = self.make_request(
+ "POST", "/_matrix/client/r0/login", json.dumps(body).encode('utf8')
+ )
+ self.render(request)
+ self.assertEqual(channel.code, 403, channel.result)