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

[1/2] Allow homeservers to send registration emails | Sending the email #5835

Merged
merged 37 commits into from
Aug 30, 2019
Merged
Show file tree
Hide file tree
Changes from 30 commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
eacd505
Allow homeservers to send registration emails
anoadragon453 Aug 8, 2019
959c051
Add changelog
anoadragon453 Aug 8, 2019
0197954
lint
anoadragon453 Aug 8, 2019
c7a2317
update
anoadragon453 Aug 8, 2019
053638d
account_threepid_delegate defaults to empty string. fix bug
anoadragon453 Aug 9, 2019
176dd5d
fix config
anoadragon453 Aug 14, 2019
73df394
lint
anoadragon453 Aug 14, 2019
c092a35
Fix lack of self.config
anoadragon453 Aug 14, 2019
616ee20
Standardize on self.store/self.config
anoadragon453 Aug 14, 2019
994c51f
Merge branch 'develop' into anoa/reg_email_sending_email
anoadragon453 Aug 14, 2019
cc5983d
Warn users with deprecated config option to update their config
anoadragon453 Aug 15, 2019
4f035bd
Fix registration email subject
anoadragon453 Aug 15, 2019
c8ba612
Actually send registration emails when registering
anoadragon453 Aug 15, 2019
7f402b1
Descope adding an email to your account
anoadragon453 Aug 15, 2019
858414f
Don't allow multiple path_regexes in client_patterns
anoadragon453 Aug 16, 2019
7e983f9
break up password reset and registration submit_token servlets
anoadragon453 Aug 16, 2019
7cd1133
lint
anoadragon453 Aug 16, 2019
6b053d3
Send emails through the configured identity server
anoadragon453 Aug 16, 2019
a6e22d7
Merge branch 'anoa/reg_email' into anoa/reg_email_sending_email
anoadragon453 Aug 19, 2019
a03cc2a
Split functionality off into other PRs
anoadragon453 Aug 19, 2019
075541a
Merge 'anoa/reg_email' into 'anoa/reg_email_sending_email'
anoadragon453 Aug 28, 2019
9e1e774
Merge branch 'anoa/reg_email' into anoa/reg_email_sending_email
anoadragon453 Aug 28, 2019
798e72b
lint
anoadragon453 Aug 28, 2019
1bc713d
Apply suggestions from code review
anoadragon453 Aug 28, 2019
70127b8
fixes from suggestions
anoadragon453 Aug 28, 2019
03d3789
Merge branch 'anoa/reg_email_sending_email' of github.com:matrix-org/…
anoadragon453 Aug 28, 2019
75b279e
Add v1.4.0 upgrade notes to UPGRADE.rst
anoadragon453 Aug 28, 2019
53c5432
Add email template information
anoadragon453 Aug 28, 2019
f14b097
lint
anoadragon453 Aug 28, 2019
6706844
Make things work again
anoadragon453 Aug 28, 2019
b29c62b
Update UPGRADE.rst to talk more about email reg
anoadragon453 Aug 29, 2019
9b1a340
Update UPGRADE.rst with reg success and fail templates
anoadragon453 Aug 29, 2019
ace8fa5
Address review comments
anoadragon453 Aug 29, 2019
5113d9e
Add password reset template information to UPGRADE.rst for Synapse v1
anoadragon453 Aug 29, 2019
06815e8
Tokens -> messages
anoadragon453 Aug 29, 2019
4dd5b97
Merge branch 'anoa/reg_email' into anoa/reg_email_sending_email
anoadragon453 Aug 29, 2019
80abdf2
Merge branch 'anoa/reg_email' into anoa/reg_email_sending_email
anoadragon453 Aug 30, 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
36 changes: 36 additions & 0 deletions UPGRADE.rst
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,42 @@ returned by the Client-Server API:
# configured on port 443.
curl -kv https://<host.name>/_matrix/client/versions 2>&1 | grep "Server:"

Upgrading to v1.4.0
anoadragon453 marked this conversation as resolved.
Show resolved Hide resolved
===================

Config options
--------------

Synapse v1.4.0 deprecates the ``email.trust_identity_server_for_password_resets`` option and
replaces it with ``account_threepid_delegate``. These options define whether the homeserver
should use an external server (typically an `identity server
<https://matrix.org/docs/spec/identity_service/r0.2.1>`_) to handle sending password reset
tokens and (as of this Synapse release) registration via a third-party address (email or phone
number).

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 address will be set to the
first entry in your 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 tokens itself. 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, the ``email`` block of the config must be configured. If
not, then password resets and registration via third-party IDs will be disabled.

Email templates
---------------

If you have configured a custom email template directory with the ``email.template_dir``
anoadragon453 marked this conversation as resolved.
Show resolved Hide resolved
option, be aware that templates ``registration.html`` and ``registration.txt`` have been
anoadragon453 marked this conversation as resolved.
Show resolved Hide resolved
added, and Synapse will expect them to exist inside the configured template directory.

To view the default email templates, see `synapse/res/templates
anoadragon453 marked this conversation as resolved.
Show resolved Hide resolved
<https://github.com/matrix-org/synapse/tree/master/synapse/res/templates>`_.

Upgrading to v1.2.0
===================

Expand Down
1 change: 1 addition & 0 deletions changelog.d/5835.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add the ability to send registration emails from the homeserver rather than delegating to an Identity Server.
11 changes: 11 additions & 0 deletions docs/sample_config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1208,11 +1208,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:
Expand Down
64 changes: 49 additions & 15 deletions synapse/config/emailconfig.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -87,7 +87,10 @@ 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:
self.account_threepid_delegate = self.trusted_third_party_id_servers[0]
anoadragon453 marked this conversation as resolved.
Show resolved Hide resolved
Expand All @@ -98,16 +101,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(
Expand All @@ -117,7 +117,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
Expand All @@ -127,7 +127,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 = []
Expand All @@ -146,28 +146,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"
anoadragon453 marked this conversation as resolved.
Show resolved Hide resolved
)
self.email_registration_template_text = email_config.get(
"registration_template_text", "registration.txt"
anoadragon453 marked this conversation as resolved.
Show resolved Hide resolved
)
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
Copy link
Member

Choose a reason for hiding this comment

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

I'm kinda wondering if a better solution is to remove the special-casing and stick them through the template engine anyway.

Copy link
Member Author

Choose a reason for hiding this comment

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

It would be more clear, but we'd have to pull the template from the disk every time we get a registration request. So a question of code quality versus performance.

Copy link
Member

Choose a reason for hiding this comment

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

given we have to do that anyway for the other templates, it feels like a hit worth taking. But also, maybe something to think about another time.

Copy link
Member Author

Choose a reason for hiding this comment

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

I'll punt it for later.

# 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):
Expand All @@ -177,9 +194,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(
anoadragon453 marked this conversation as resolved.
Show resolved Hide resolved
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 = [
Expand Down Expand Up @@ -291,11 +314,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
"""


Expand Down
4 changes: 2 additions & 2 deletions synapse/handlers/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"],
Expand Down
79 changes: 79 additions & 0 deletions synapse/handlers/identity.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down
12 changes: 0 additions & 12 deletions synapse/handlers/register.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
34 changes: 29 additions & 5 deletions synapse/push/mailer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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] == "":
richvdh marked this conversation as resolved.
Show resolved Hide resolved
raise RuntimeError("Invalid 'to' email address")

link = (
self.hs.config.public_baseurl
+ "_matrix/client/unstable/password_reset/email/submit_token"
Expand All @@ -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"
anoadragon453 marked this conversation as resolved.
Show resolved Hide resolved
"?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,
)

Expand Down
Loading