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

Send password reset from HS: Sending the email #5345

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
dbdebc2
Ability to send password reset emails
anoadragon453 May 24, 2019
9567c60
Merge branch 'develop' into anoa/hs_password_reset_sending_email
anoadragon453 Jun 4, 2019
ed35302
Fix validation token lifetime email_ prefix
anoadragon453 Jun 4, 2019
094c351
Add changelog
anoadragon453 Jun 4, 2019
899219c
Update manifest to include txt/html template files
anoadragon453 Jun 5, 2019
309943f
Update db
anoadragon453 Jun 5, 2019
354d749
mark jinja2 and bleach as required dependencies
anoadragon453 Jun 5, 2019
62e1ec0
Add email settings to default unit test config
anoadragon453 Jun 5, 2019
a0e2a10
Update unit test template dir
anoadragon453 Jun 5, 2019
a862f2a
gen sample config
anoadragon453 Jun 5, 2019
752dbee
Merge branch 'anoa/feature_hs_password_resets' into anoa/hs_password_…
anoadragon453 Jun 5, 2019
177f024
Add html5lib as a required dep
anoadragon453 Jun 5, 2019
6d2d3c9
Modify check for smtp settings to be kinder to CI
anoadragon453 Jun 5, 2019
6394715
silly linting rules
anoadragon453 Jun 5, 2019
fe0af29
Correct html5lib dep version number
anoadragon453 Jun 5, 2019
91eac88
one more time
anoadragon453 Jun 5, 2019
c9573ca
Change template_dir to originate from synapse root dir
anoadragon453 Jun 5, 2019
4c406f5
Revert "Modify check for smtp settings to be kinder to CI"
anoadragon453 Jun 5, 2019
70b161d
Move templates. New option to disable password resets
anoadragon453 Jun 5, 2019
79bc668
Update templates and make password reset option work
anoadragon453 Jun 5, 2019
f522cde
Change jinja2 and bleach back to opt deps
anoadragon453 Jun 5, 2019
a4c0907
Update email condition requirement
anoadragon453 Jun 5, 2019
efa1a56
Only import jinja2/bleach if we need it
anoadragon453 Jun 5, 2019
6a9588c
Update sample config
anoadragon453 Jun 5, 2019
78ca92a
Revert manifest changes for new res directory
anoadragon453 Jun 5, 2019
12ed769
Remove public_baseurl from unittest config
anoadragon453 Jun 5, 2019
6efb301
infer ability to reset password from email config
anoadragon453 Jun 5, 2019
3478213
Address review comments
anoadragon453 Jun 6, 2019
a37a2f1
regen sample config
anoadragon453 Jun 6, 2019
cd4f4a2
test for ci
anoadragon453 Jun 6, 2019
92090d3
Remove CI test
anoadragon453 Jun 6, 2019
7168dee
fix bug?
anoadragon453 Jun 6, 2019
828cdbb
Run bg update on the master process
anoadragon453 Jun 6, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/5345.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add ability to perform password reset via email without trusting the identity server.
56 changes: 45 additions & 11 deletions docs/sample_config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1018,33 +1018,67 @@ password_config:



# Enable sending emails for notification events or expiry notices
# Defining a custom URL for Riot is only needed if email notifications
# should contain links to a self-hosted installation of Riot; when set
# the "app_name" setting is ignored.
# Enable sending emails for password resets, notification events or
# account expiry notices.
#
# If your SMTP server requires authentication, the optional smtp_user &
# smtp_pass variables should be used
#
#email:
# enable_notifs: false
# enable_notifs: False
# smtp_host: "localhost"
# smtp_port: 25
# smtp_port: 25 # SSL: 465, STARTTLS: 587
# smtp_user: "exampleusername"
# smtp_pass: "examplepassword"
# require_transport_security: False
# notif_from: "Your Friendly %(app)s Home Server <[email protected]>"
# app_name: Matrix
# # if template_dir is unset, uses the example templates that are part of
# # the Synapse distribution.
#
# # Enable email notifications by default
# notif_for_new_users: True
#
# # Defining a custom URL for Riot is only needed if email notifications
# # should contain links to a self-hosted installation of Riot; when set
# # the "app_name" setting is ignored
# riot_base_url: "http://localhost/riot"
#
# # Enable sending password reset emails via the configured, trusted
# # identity servers
# #
# # IMPORTANT! This will give a malicious or overtaken identity server
# # the ability to reset passwords for your users! Make absolutely sure
# # that you want to do this! It is strongly recommended that password
# # reset emails be sent by the homeserver instead
# #
# # If this option is set to false and SMTP options have not been
# # configured, resetting user passwords via email will be disabled
# #trust_identity_server_for_password_resets: false
#
# # Configure the time that a validation email or text message code
# # will expire after sending
# #
# # This is currently used for password resets
# #validation_token_lifetime: 1h
#
# # Template directory. All template files should be stored within this
# # directory
# #
# #template_dir: res/templates
#
# # Templates for email notifications
# #
# notif_template_html: notif_mail.html
# notif_template_text: notif_mail.txt
# # Templates for account expiry notices.
#
# # Templates for account expiry notices
# #
# expiry_template_html: notice_expiry.html
# expiry_template_text: notice_expiry.txt
# notif_for_new_users: True
# riot_base_url: "http://localhost/riot"
#
# # Templates for password reset emails sent by the homeserver
# #
# #password_reset_template_html: password_reset.html
# #password_reset_template_text: password_reset.txt


#password_providers:
Expand Down
125 changes: 113 additions & 12 deletions synapse/config/emailconfig.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,11 @@ def read_config(self, config):
else:
self.email_app_name = "Matrix"

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

if self.email_enable_notifs or account_validity_renewal_enabled:
email_trust_identity_server_for_password_resets = email_config.get(
"trust_identity_server_for_password_resets", False,
)
self.email_password_reset_behaviour = (
"remote" if email_trust_identity_server_for_password_resets else "local"
)
if self.email_password_reset_behaviour == "local" and email_config == {}:
logger.warn(
"User password resets have been disabled due to lack of email config"
)
self.email_password_reset_behaviour = "off"

# Get lifetime of a validation token in milliseconds
self.email_validation_token_lifetime = self.parse_duration(
email_config.get("validation_token_lifetime", "1h")
)

if (
self.email_enable_notifs
or account_validity_renewal_enabled
or self.email_password_reset_behaviour == "local"
):
# make sure we can import the required deps
import jinja2
import bleach
# prevent unused warnings
jinja2
bleach

if self.email_password_reset_behaviour == "local":
required = [
"smtp_host",
"smtp_port",
"notif_from",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should probably call this something else....

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, but, backwards compatibility.

]

missing = []
for k in required:
if k not in email_config:
missing.append(k)

if (len(missing) > 0):
raise RuntimeError(
"email.password_reset_behaviour is set to 'local' "
"but required keys are missing: %s" %
(", ".join(["email." + k for k in missing]),)
)

# Templates for password reset emails
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",
)

# Check templates exist
for f in [self.email_password_reset_template_html,
self.email_password_reset_template_text]:
p = os.path.join(self.email_template_dir, f)
if not os.path.isfile(p):
raise ConfigError("Unable to find template file %s" % (p, ))

if config.get("public_baseurl") is None:
raise RuntimeError(
"email.password_reset_behaviour is set to 'local' but no "
"public_baseurl is set. This is necessary to generate password "
"reset links"
)

if self.email_enable_notifs:
required = [
"smtp_host",
Expand Down Expand Up @@ -141,31 +208,65 @@ def read_config(self, config):

def default_config(self, config_dir_path, server_name, **kwargs):
return """
# Enable sending emails for notification events or expiry notices
# Defining a custom URL for Riot is only needed if email notifications
# should contain links to a self-hosted installation of Riot; when set
# the "app_name" setting is ignored.
# Enable sending emails for password resets, notification events or
# account expiry notices.
#
# If your SMTP server requires authentication, the optional smtp_user &
# smtp_pass variables should be used
#
#email:
# enable_notifs: false
# enable_notifs: False
# smtp_host: "localhost"
# smtp_port: 25
# smtp_port: 25 # SSL: 465, STARTTLS: 587
# smtp_user: "exampleusername"
# smtp_pass: "examplepassword"
# require_transport_security: False
# notif_from: "Your Friendly %(app)s Home Server <[email protected]>"
# app_name: Matrix
# # if template_dir is unset, uses the example templates that are part of
# # the Synapse distribution.
#
# # Enable email notifications by default
# notif_for_new_users: True
#
# # Defining a custom URL for Riot is only needed if email notifications
# # should contain links to a self-hosted installation of Riot; when set
# # the "app_name" setting is ignored
# riot_base_url: "http://localhost/riot"
#
# # Enable sending password reset emails via the configured, trusted
# # identity servers
# #
# # IMPORTANT! This will give a malicious or overtaken identity server
# # the ability to reset passwords for your users! Make absolutely sure
# # that you want to do this! It is strongly recommended that password
# # reset emails be sent by the homeserver instead
# #
# # If this option is set to false and SMTP options have not been
# # configured, resetting user passwords via email will be disabled
# #trust_identity_server_for_password_resets: false
#
# # Configure the time that a validation email or text message code
# # will expire after sending
# #
# # This is currently used for password resets
# #validation_token_lifetime: 1h
#
# # Template directory. All template files should be stored within this
# # directory
# #
# #template_dir: res/templates
#
# # Templates for email notifications
# #
# notif_template_html: notif_mail.html
# notif_template_text: notif_mail.txt
# # Templates for account expiry notices.
#
# # Templates for account expiry notices
# #
# expiry_template_html: notice_expiry.html
# expiry_template_text: notice_expiry.txt
# notif_for_new_users: True
# riot_base_url: "http://localhost/riot"
#
# # Templates for password reset emails sent by the homeserver
# #
# #password_reset_template_html: password_reset.html
# #password_reset_template_text: password_reset.txt
"""
2 changes: 1 addition & 1 deletion synapse/config/tls.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ def read_config(self, config):
certs = []
for ca_file in custom_ca_list:
logger.debug("Reading custom CA certificate file: %s", ca_file)
content = self.read_file(ca_file)
content = self.read_file(ca_file, "federation_custom_ca_list")

# Parse the CA certificates
try:
Expand Down
13 changes: 11 additions & 2 deletions synapse/handlers/identity.py
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,14 @@ def try_unbind_threepid_with_id_server(self, mxid, threepid, id_server):
defer.returnValue(changed)

@defer.inlineCallbacks
def requestEmailToken(self, id_server, email, client_secret, send_attempt, **kwargs):
def requestEmailToken(
self,
id_server,
email,
client_secret,
send_attempt,
next_link=None,
):
if not self._should_trust_id_server(id_server):
raise SynapseError(
400, "Untrusted ID server '%s'" % id_server,
Expand All @@ -259,7 +266,9 @@ def requestEmailToken(self, id_server, email, client_secret, send_attempt, **kwa
'client_secret': client_secret,
'send_attempt': send_attempt,
}
params.update(kwargs)

if next_link:
params.update({'next_link': next_link})

try:
data = yield self.http_client.post_json_get_json(
Expand Down
85 changes: 67 additions & 18 deletions synapse/push/mailer.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,10 +80,10 @@


class Mailer(object):
def __init__(self, hs, app_name, notif_template_html, notif_template_text):
def __init__(self, hs, app_name, template_html, template_text):
self.hs = hs
self.notif_template_html = notif_template_html
self.notif_template_text = notif_template_text
self.template_html = template_html
self.template_text = template_text

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

@defer.inlineCallbacks
def send_notification_mail(self, app_id, user_id, email_address,
push_actions, reason):
try:
from_string = self.hs.config.email_notif_from % {
"app": self.app_name
}
except TypeError:
from_string = self.hs.config.email_notif_from
def send_password_reset_mail(
self,
email_address,
token,
client_secret,
sid,
):
"""Send an email with a password reset link to a user

Args:
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
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 +
"_synapse/password_reset/email/submit_token"
"?token=%s&client_secret=%s&sid=%s" %
(token, client_secret, sid)
)

raw_from = email.utils.parseaddr(from_string)[1]
raw_to = email.utils.parseaddr(email_address)[1]
template_vars = {
"link": link,
}

if raw_to == '':
raise RuntimeError("Invalid 'to' address")
yield self.send_email(
email_address,
"[%s] Password Reset Email" % self.hs.config.server_name,
template_vars,
)

@defer.inlineCallbacks
def send_notification_mail(self, app_id, user_id, email_address,
push_actions, reason):
"""Send email regarding a user's room notifications"""
rooms_in_order = deduped_ordered_list(
[pa['room_id'] for pa in push_actions]
)
Expand Down Expand Up @@ -176,14 +203,36 @@ def _fetch_room_state(room_id):
"reason": reason,
}

html_text = self.notif_template_html.render(**template_vars)
yield self.send_email(
email_address,
"[%s] %s" % (self.app_name, summary_text),
template_vars,
)

@defer.inlineCallbacks
def send_email(self, email_address, subject, template_vars):
"""Send an email with the given information and template text"""
try:
from_string = self.hs.config.email_notif_from % {
"app": self.app_name
}
except TypeError:
from_string = self.hs.config.email_notif_from

raw_from = email.utils.parseaddr(from_string)[1]
raw_to = email.utils.parseaddr(email_address)[1]

if raw_to == '':
raise RuntimeError("Invalid 'to' address")

html_text = self.template_html.render(**template_vars)
html_part = MIMEText(html_text, "html", "utf8")

plain_text = self.notif_template_text.render(**template_vars)
plain_text = self.template_text.render(**template_vars)
text_part = MIMEText(plain_text, "plain", "utf8")

multipart_msg = MIMEMultipart('alternative')
multipart_msg['Subject'] = "[%s] %s" % (self.app_name, summary_text)
multipart_msg['Subject'] = subject
multipart_msg['From'] = from_string
multipart_msg['To'] = email_address
multipart_msg['Date'] = email.utils.formatdate()
Expand Down
Loading