Skip to content
This repository was archived by the owner on Apr 26, 2024. It is now read-only.

Commit 8dba4ba

Browse files
Send password reset from HS: Sending the email (#5345)
* 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. * Fix validation token lifetime email_ prefix * Add changelog * Update manifest to include txt/html template files * Update db * mark jinja2 and bleach as required dependencies * Add email settings to default unit test config * Update unit test template dir * gen sample config * Add html5lib as a required dep * Modify check for smtp settings to be kinder to CI * silly linting rules * Correct html5lib dep version number * one more time * Change template_dir to originate from synapse root dir * Revert "Modify check for smtp settings to be kinder to CI" This reverts commit 6d2d3c9. * Move templates. New option to disable password resets * Update templates and make password reset option work * Change jinja2 and bleach back to opt deps * Update email condition requirement * Only import jinja2/bleach if we need it * Update sample config * Revert manifest changes for new res directory * Remove public_baseurl from unittest config * infer ability to reset password from email config * Address review comments * regen sample config * test for ci * Remove CI test * fix bug? * Run bg update on the master process
1 parent 24f31df commit 8dba4ba

File tree

14 files changed

+452
-57
lines changed

14 files changed

+452
-57
lines changed

changelog.d/5345.feature

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add ability to perform password reset via email without trusting the identity server.

docs/sample_config.yaml

Lines changed: 45 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1018,33 +1018,67 @@ password_config:
10181018

10191019

10201020

1021-
# Enable sending emails for notification events or expiry notices
1022-
# Defining a custom URL for Riot is only needed if email notifications
1023-
# should contain links to a self-hosted installation of Riot; when set
1024-
# the "app_name" setting is ignored.
1021+
# Enable sending emails for password resets, notification events or
1022+
# account expiry notices.
10251023
#
10261024
# If your SMTP server requires authentication, the optional smtp_user &
10271025
# smtp_pass variables should be used
10281026
#
10291027
#email:
1030-
# enable_notifs: false
1028+
# enable_notifs: False
10311029
# smtp_host: "localhost"
1032-
# smtp_port: 25
1030+
# smtp_port: 25 # SSL: 465, STARTTLS: 587
10331031
# smtp_user: "exampleusername"
10341032
# smtp_pass: "examplepassword"
10351033
# require_transport_security: False
10361034
# notif_from: "Your Friendly %(app)s Home Server <[email protected]>"
10371035
# app_name: Matrix
1038-
# # if template_dir is unset, uses the example templates that are part of
1039-
# # the Synapse distribution.
1036+
#
1037+
# # Enable email notifications by default
1038+
# notif_for_new_users: True
1039+
#
1040+
# # Defining a custom URL for Riot is only needed if email notifications
1041+
# # should contain links to a self-hosted installation of Riot; when set
1042+
# # the "app_name" setting is ignored
1043+
# riot_base_url: "http://localhost/riot"
1044+
#
1045+
# # Enable sending password reset emails via the configured, trusted
1046+
# # identity servers
1047+
# #
1048+
# # IMPORTANT! This will give a malicious or overtaken identity server
1049+
# # the ability to reset passwords for your users! Make absolutely sure
1050+
# # that you want to do this! It is strongly recommended that password
1051+
# # reset emails be sent by the homeserver instead
1052+
# #
1053+
# # If this option is set to false and SMTP options have not been
1054+
# # configured, resetting user passwords via email will be disabled
1055+
# #trust_identity_server_for_password_resets: false
1056+
#
1057+
# # Configure the time that a validation email or text message code
1058+
# # will expire after sending
1059+
# #
1060+
# # This is currently used for password resets
1061+
# #validation_token_lifetime: 1h
1062+
#
1063+
# # Template directory. All template files should be stored within this
1064+
# # directory
1065+
# #
10401066
# #template_dir: res/templates
1067+
#
1068+
# # Templates for email notifications
1069+
# #
10411070
# notif_template_html: notif_mail.html
10421071
# notif_template_text: notif_mail.txt
1043-
# # Templates for account expiry notices.
1072+
#
1073+
# # Templates for account expiry notices
1074+
# #
10441075
# expiry_template_html: notice_expiry.html
10451076
# expiry_template_text: notice_expiry.txt
1046-
# notif_for_new_users: True
1047-
# riot_base_url: "http://localhost/riot"
1077+
#
1078+
# # Templates for password reset emails sent by the homeserver
1079+
# #
1080+
# #password_reset_template_html: password_reset.html
1081+
# #password_reset_template_text: password_reset.txt
10481082

10491083

10501084
#password_providers:

synapse/config/emailconfig.py

Lines changed: 113 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,11 @@ def read_config(self, config):
5050
else:
5151
self.email_app_name = "Matrix"
5252

53+
# TODO: Rename notif_from to something more generic, or have a separate
54+
# from for password resets, message notifications, etc?
55+
# Currently the email section is a bit bogged down with settings for
56+
# multiple functions. Would be good to split it out into separate
57+
# sections and only put the common ones under email:
5358
self.email_notif_from = email_config.get("notif_from", None)
5459
if self.email_notif_from is not None:
5560
# make sure it's valid
@@ -74,14 +79,76 @@ def read_config(self, config):
7479
"account_validity", {},
7580
).get("renew_at")
7681

77-
if self.email_enable_notifs or account_validity_renewal_enabled:
82+
email_trust_identity_server_for_password_resets = email_config.get(
83+
"trust_identity_server_for_password_resets", False,
84+
)
85+
self.email_password_reset_behaviour = (
86+
"remote" if email_trust_identity_server_for_password_resets else "local"
87+
)
88+
if self.email_password_reset_behaviour == "local" and email_config == {}:
89+
logger.warn(
90+
"User password resets have been disabled due to lack of email config"
91+
)
92+
self.email_password_reset_behaviour = "off"
93+
94+
# Get lifetime of a validation token in milliseconds
95+
self.email_validation_token_lifetime = self.parse_duration(
96+
email_config.get("validation_token_lifetime", "1h")
97+
)
98+
99+
if (
100+
self.email_enable_notifs
101+
or account_validity_renewal_enabled
102+
or self.email_password_reset_behaviour == "local"
103+
):
78104
# make sure we can import the required deps
79105
import jinja2
80106
import bleach
81107
# prevent unused warnings
82108
jinja2
83109
bleach
84110

111+
if self.email_password_reset_behaviour == "local":
112+
required = [
113+
"smtp_host",
114+
"smtp_port",
115+
"notif_from",
116+
]
117+
118+
missing = []
119+
for k in required:
120+
if k not in email_config:
121+
missing.append(k)
122+
123+
if (len(missing) > 0):
124+
raise RuntimeError(
125+
"email.password_reset_behaviour is set to 'local' "
126+
"but required keys are missing: %s" %
127+
(", ".join(["email." + k for k in missing]),)
128+
)
129+
130+
# Templates for password reset emails
131+
self.email_password_reset_template_html = email_config.get(
132+
"password_reset_template_html", "password_reset.html",
133+
)
134+
self.email_password_reset_template_text = email_config.get(
135+
"password_reset_template_text", "password_reset.txt",
136+
)
137+
138+
# Check templates exist
139+
for f in [self.email_password_reset_template_html,
140+
self.email_password_reset_template_text]:
141+
p = os.path.join(self.email_template_dir, f)
142+
if not os.path.isfile(p):
143+
raise ConfigError("Unable to find template file %s" % (p, ))
144+
145+
if config.get("public_baseurl") is None:
146+
raise RuntimeError(
147+
"email.password_reset_behaviour is set to 'local' but no "
148+
"public_baseurl is set. This is necessary to generate password "
149+
"reset links"
150+
)
151+
85152
if self.email_enable_notifs:
86153
required = [
87154
"smtp_host",
@@ -141,31 +208,65 @@ def read_config(self, config):
141208

142209
def default_config(self, config_dir_path, server_name, **kwargs):
143210
return """
144-
# Enable sending emails for notification events or expiry notices
145-
# Defining a custom URL for Riot is only needed if email notifications
146-
# should contain links to a self-hosted installation of Riot; when set
147-
# the "app_name" setting is ignored.
211+
# Enable sending emails for password resets, notification events or
212+
# account expiry notices.
148213
#
149214
# If your SMTP server requires authentication, the optional smtp_user &
150215
# smtp_pass variables should be used
151216
#
152217
#email:
153-
# enable_notifs: false
218+
# enable_notifs: False
154219
# smtp_host: "localhost"
155-
# smtp_port: 25
220+
# smtp_port: 25 # SSL: 465, STARTTLS: 587
156221
# smtp_user: "exampleusername"
157222
# smtp_pass: "examplepassword"
158223
# require_transport_security: False
159224
# notif_from: "Your Friendly %(app)s Home Server <[email protected]>"
160225
# app_name: Matrix
161-
# # if template_dir is unset, uses the example templates that are part of
162-
# # the Synapse distribution.
226+
#
227+
# # Enable email notifications by default
228+
# notif_for_new_users: True
229+
#
230+
# # Defining a custom URL for Riot is only needed if email notifications
231+
# # should contain links to a self-hosted installation of Riot; when set
232+
# # the "app_name" setting is ignored
233+
# riot_base_url: "http://localhost/riot"
234+
#
235+
# # Enable sending password reset emails via the configured, trusted
236+
# # identity servers
237+
# #
238+
# # IMPORTANT! This will give a malicious or overtaken identity server
239+
# # the ability to reset passwords for your users! Make absolutely sure
240+
# # that you want to do this! It is strongly recommended that password
241+
# # reset emails be sent by the homeserver instead
242+
# #
243+
# # If this option is set to false and SMTP options have not been
244+
# # configured, resetting user passwords via email will be disabled
245+
# #trust_identity_server_for_password_resets: false
246+
#
247+
# # Configure the time that a validation email or text message code
248+
# # will expire after sending
249+
# #
250+
# # This is currently used for password resets
251+
# #validation_token_lifetime: 1h
252+
#
253+
# # Template directory. All template files should be stored within this
254+
# # directory
255+
# #
163256
# #template_dir: res/templates
257+
#
258+
# # Templates for email notifications
259+
# #
164260
# notif_template_html: notif_mail.html
165261
# notif_template_text: notif_mail.txt
166-
# # Templates for account expiry notices.
262+
#
263+
# # Templates for account expiry notices
264+
# #
167265
# expiry_template_html: notice_expiry.html
168266
# expiry_template_text: notice_expiry.txt
169-
# notif_for_new_users: True
170-
# riot_base_url: "http://localhost/riot"
267+
#
268+
# # Templates for password reset emails sent by the homeserver
269+
# #
270+
# #password_reset_template_html: password_reset.html
271+
# #password_reset_template_text: password_reset.txt
171272
"""

synapse/config/tls.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ def read_config(self, config):
107107
certs = []
108108
for ca_file in custom_ca_list:
109109
logger.debug("Reading custom CA certificate file: %s", ca_file)
110-
content = self.read_file(ca_file)
110+
content = self.read_file(ca_file, "federation_custom_ca_list")
111111

112112
# Parse the CA certificates
113113
try:

synapse/handlers/identity.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -247,7 +247,14 @@ def try_unbind_threepid_with_id_server(self, mxid, threepid, id_server):
247247
defer.returnValue(changed)
248248

249249
@defer.inlineCallbacks
250-
def requestEmailToken(self, id_server, email, client_secret, send_attempt, **kwargs):
250+
def requestEmailToken(
251+
self,
252+
id_server,
253+
email,
254+
client_secret,
255+
send_attempt,
256+
next_link=None,
257+
):
251258
if not self._should_trust_id_server(id_server):
252259
raise SynapseError(
253260
400, "Untrusted ID server '%s'" % id_server,
@@ -259,7 +266,9 @@ def requestEmailToken(self, id_server, email, client_secret, send_attempt, **kwa
259266
'client_secret': client_secret,
260267
'send_attempt': send_attempt,
261268
}
262-
params.update(kwargs)
269+
270+
if next_link:
271+
params.update({'next_link': next_link})
263272

264273
try:
265274
data = yield self.http_client.post_json_get_json(

synapse/push/mailer.py

Lines changed: 67 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -80,10 +80,10 @@
8080

8181

8282
class Mailer(object):
83-
def __init__(self, hs, app_name, notif_template_html, notif_template_text):
83+
def __init__(self, hs, app_name, template_html, template_text):
8484
self.hs = hs
85-
self.notif_template_html = notif_template_html
86-
self.notif_template_text = notif_template_text
85+
self.template_html = template_html
86+
self.template_text = template_text
8787

8888
self.sendmail = self.hs.get_sendmail()
8989
self.store = self.hs.get_datastore()
@@ -94,21 +94,48 @@ def __init__(self, hs, app_name, notif_template_html, notif_template_text):
9494
logger.info("Created Mailer for app_name %s" % app_name)
9595

9696
@defer.inlineCallbacks
97-
def send_notification_mail(self, app_id, user_id, email_address,
98-
push_actions, reason):
99-
try:
100-
from_string = self.hs.config.email_notif_from % {
101-
"app": self.app_name
102-
}
103-
except TypeError:
104-
from_string = self.hs.config.email_notif_from
97+
def send_password_reset_mail(
98+
self,
99+
email_address,
100+
token,
101+
client_secret,
102+
sid,
103+
):
104+
"""Send an email with a password reset link to a user
105+
106+
Args:
107+
email_address (str): Email address we're sending the password
108+
reset to
109+
token (str): Unique token generated by the server to verify
110+
password reset email was received
111+
client_secret (str): Unique token generated by the client to
112+
group together multiple email sending attempts
113+
sid (str): The generated session ID
114+
"""
115+
if email.utils.parseaddr(email_address)[1] == '':
116+
raise RuntimeError("Invalid 'to' email address")
117+
118+
link = (
119+
self.hs.config.public_baseurl +
120+
"_synapse/password_reset/email/submit_token"
121+
"?token=%s&client_secret=%s&sid=%s" %
122+
(token, client_secret, sid)
123+
)
105124

106-
raw_from = email.utils.parseaddr(from_string)[1]
107-
raw_to = email.utils.parseaddr(email_address)[1]
125+
template_vars = {
126+
"link": link,
127+
}
108128

109-
if raw_to == '':
110-
raise RuntimeError("Invalid 'to' address")
129+
yield self.send_email(
130+
email_address,
131+
"[%s] Password Reset Email" % self.hs.config.server_name,
132+
template_vars,
133+
)
111134

135+
@defer.inlineCallbacks
136+
def send_notification_mail(self, app_id, user_id, email_address,
137+
push_actions, reason):
138+
"""Send email regarding a user's room notifications"""
112139
rooms_in_order = deduped_ordered_list(
113140
[pa['room_id'] for pa in push_actions]
114141
)
@@ -176,14 +203,36 @@ def _fetch_room_state(room_id):
176203
"reason": reason,
177204
}
178205

179-
html_text = self.notif_template_html.render(**template_vars)
206+
yield self.send_email(
207+
email_address,
208+
"[%s] %s" % (self.app_name, summary_text),
209+
template_vars,
210+
)
211+
212+
@defer.inlineCallbacks
213+
def send_email(self, email_address, subject, template_vars):
214+
"""Send an email with the given information and template text"""
215+
try:
216+
from_string = self.hs.config.email_notif_from % {
217+
"app": self.app_name
218+
}
219+
except TypeError:
220+
from_string = self.hs.config.email_notif_from
221+
222+
raw_from = email.utils.parseaddr(from_string)[1]
223+
raw_to = email.utils.parseaddr(email_address)[1]
224+
225+
if raw_to == '':
226+
raise RuntimeError("Invalid 'to' address")
227+
228+
html_text = self.template_html.render(**template_vars)
180229
html_part = MIMEText(html_text, "html", "utf8")
181230

182-
plain_text = self.notif_template_text.render(**template_vars)
231+
plain_text = self.template_text.render(**template_vars)
183232
text_part = MIMEText(plain_text, "plain", "utf8")
184233

185234
multipart_msg = MIMEMultipart('alternative')
186-
multipart_msg['Subject'] = "[%s] %s" % (self.app_name, summary_text)
235+
multipart_msg['Subject'] = subject
187236
multipart_msg['From'] = from_string
188237
multipart_msg['To'] = email_address
189238
multipart_msg['Date'] = email.utils.formatdate()

0 commit comments

Comments
 (0)