Allow Synapse to send registration emails + choose Synapse or an external server to handle 3pid validation (#5987)

This is a combination of a few different PRs, finally all being merged into `develop`:

* #5875 
* #5876 
* #5868 (This one added the `/versions` flag but the flag itself was actually [backed out](891afb57cb (diff-e591d42d30690ffb79f63bb726200891)) in #5969. What's left is just giving /versions access to the config file, which could be useful in the future)
* #5835 
* #5969 
* #5940

Clients should not actually use the new registration functionality until https://github.com/matrix-org/synapse/pull/5972 is merged.

UPGRADE.rst, changelog entries and config file changes should all be reviewed closely before this PR is merged.
This commit is contained in:
Andrew Morgan 2019-09-06 11:35:28 +01:00 committed by GitHub
parent f7c873a643
commit 0c0b82b6d1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
29 changed files with 822 additions and 304 deletions

View file

@ -49,6 +49,56 @@ returned by the Client-Server API:
# configured on port 443. # configured on port 443.
curl -kv https://<host.name>/_matrix/client/versions 2>&1 | grep "Server:" curl -kv https://<host.name>/_matrix/client/versions 2>&1 | grep "Server:"
Upgrading to v1.4.0
===================
Config options
--------------
**Note: Registration by email address or phone number will not work in this release unless
some config options are changed from their defaults.**
This is due to Synapse v1.4.0 now defaulting to sending registration and password reset tokens
itself. This is for security reasons as well as putting less reliance on identity servers.
However, 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, phone-based password resets and registration will be disabled. For Synapse to send
emails, the ``email`` block of the config must be filled out. If not, then password resets and
registration via email will be disabled entirely.
This release also deprecates the ``email.trust_identity_server_for_password_resets`` option and
replaces it with the ``account_threepid_delegates`` dictionary. This option defines whether the
homeserver should delegate an external server (typically an `identity server
<https://matrix.org/docs/spec/identity_service/r0.2.1>`_) to handle sending password reset or
registration messages via email and SMS.
If ``email.trust_identity_server_for_password_resets`` is set to ``true``, and
``account_threepid_delegates.email`` is not set, then the first entry in
``trusted_third_party_id_servers`` will be used as the account threepid delegate for email.
This is to ensure compatibility with existing Synapse installs that set up external server
handling for these tasks before v1.4.0. If ``email.trust_identity_server_for_password_resets``
is ``true`` and no trusted identity server domains are configured, Synapse will throw an error.
If ``email.trust_identity_server_for_password_resets`` is ``false`` or absent and a threepid
type in ``account_threepid_delegates`` is not set to a domain, then Synapse will attempt to
send password reset and registration messages for that type.
Email templates
---------------
If you have configured a custom template directory with the ``email.template_dir`` option, be
aware that there are new templates regarding registration. ``registration.html`` and
``registration.txt`` have been added and contain the content that is sent to a client upon
registering via an email address.
``registration_success.html`` and ``registration_failure.html`` are also new HTML templates
that will be shown to the user when they click the link in their registration emai , either
showing them a success or failure page (assuming a redirect URL is not configured).
Synapse will expect these files to exist inside the configured template directory. To view the
default templates, see `synapse/res/templates
<https://github.com/matrix-org/synapse/tree/master/synapse/res/templates>`_.
Upgrading to v1.2.0 Upgrading to v1.2.0
=================== ===================
@ -132,6 +182,19 @@ server for password resets, set ``trust_identity_server_for_password_resets`` to
See the `sample configuration file <docs/sample_config.yaml>`_ See the `sample configuration file <docs/sample_config.yaml>`_
for more details on these settings. for more details on these settings.
New email templates
---------------
Some new templates have been added to the default template directory for the purpose of the
homeserver sending its own password reset emails. If you have configured a custom
``template_dir`` in your Synapse config, these files will need to be added.
``password_reset.html`` and ``password_reset.txt`` are HTML and plain text templates
respectively that contain the contents of what will be emailed to the user upon attempting to
reset their password via email. ``password_reset_success.html`` and
``password_reset_failure.html`` are HTML files that the content of which (assuming no redirect
URL is set) will be shown to the user after they attempt to click the link in the email sent
to them.
Upgrading to v0.99.0 Upgrading to v0.99.0
==================== ====================

1
changelog.d/5835.feature Normal file
View file

@ -0,0 +1 @@
Add the ability to send registration emails from the homeserver rather than delegating to an identity server.

1
changelog.d/5868.feature Normal file
View file

@ -0,0 +1 @@
Add `m.require_identity_server` key to `/versions`'s `unstable_features` section.

1
changelog.d/5875.misc Normal file
View file

@ -0,0 +1 @@
Deprecate the `trusted_third_party_id_servers` option.

1
changelog.d/5876.feature Normal file
View file

@ -0,0 +1 @@
Replace `trust_identity_server_for_password_resets` config option with `account_threepid_delegates`.

1
changelog.d/5940.feature Normal file
View file

@ -0,0 +1 @@
Add the ability to send registration emails from the homeserver rather than delegating to an identity server.

1
changelog.d/5969.feature Normal file
View file

@ -0,0 +1 @@
Replace `trust_identity_server_for_password_resets` config option with `account_threepid_delegates`.

View file

@ -37,6 +37,8 @@ from signedjson.sign import verify_signed_json, SignatureVerifyException
CONFIG_JSON = "cmdclient_config.json" CONFIG_JSON = "cmdclient_config.json"
# TODO: The concept of trusted identity servers has been deprecated. This option and checks
# should be removed
TRUSTED_ID_SERVERS = ["localhost:8001"] TRUSTED_ID_SERVERS = ["localhost:8001"]

View file

@ -891,10 +891,42 @@ uploads_path: "DATADIR/uploads"
# Also defines the ID server which will be called when an account is # Also defines the ID server which will be called when an account is
# deactivated (one will be picked arbitrarily). # deactivated (one will be picked arbitrarily).
# #
# Note: This option is deprecated. Since v0.99.4, Synapse has tracked which identity
# server a 3PID has been bound to. For 3PIDs bound before then, Synapse runs a
# background migration script, informing itself that the identity server all of its
# 3PIDs have been bound to is likely one of the below.
#
# As of Synapse v1.4.0, all other functionality of this option has been deprecated, and
# it is now solely used for the purposes of the background migration script, and can be
# removed once it has run.
#trusted_third_party_id_servers: #trusted_third_party_id_servers:
# - matrix.org # - matrix.org
# - vector.im # - vector.im
# Handle threepid (email/phone etc) registration and password resets through a set of
# *trusted* identity servers. Note that this allows the configured identity server to
# reset passwords for accounts!
#
# Be aware that if `email` is not set, and SMTP options have not been
# configured in the email config block, registration and user password resets via
# email will be globally disabled.
#
# Additionally, if `msisdn` is not set, registration and password resets via msisdn
# will be disabled regardless. This is due to Synapse currently not supporting any
# method of sending SMS messages on its own.
#
# To enable using an identity server for operations regarding a particular third-party
# identifier type, set the value to the URL of that identity server as shown in the
# examples below.
#
# Servers handling the these requests must answer the `/requestToken` endpoints defined
# by the Matrix Identity Service API specification:
# https://matrix.org/docs/spec/identity_service/latest
#
account_threepid_delegates:
#email: https://example.com # Delegate email sending to matrix.org
#msisdn: http://localhost:8090 # Delegate SMS sending to this local process
# Users who register on this homeserver will automatically be joined # Users who register on this homeserver will automatically be joined
# to these rooms # to these rooms
# #
@ -1164,19 +1196,6 @@ password_config:
# # # #
# riot_base_url: "http://localhost/riot" # 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 # # Configure the time that a validation email or text message code
# # will expire after sending # # will expire after sending
# # # #
@ -1208,11 +1227,22 @@ password_config:
# #password_reset_template_html: password_reset.html # #password_reset_template_html: password_reset.html
# #password_reset_template_text: password_reset.txt # #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 # # Templates for password reset success and failure pages that a user
# # will see after attempting to reset their password # # will see after attempting to reset their password
# # # #
# #password_reset_template_success_html: password_reset_success.html # #password_reset_template_success_html: password_reset_success.html
# #password_reset_template_failure_html: password_reset_failure.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: #password_providers:

View file

@ -119,7 +119,7 @@ class ClientReaderServer(HomeServer):
KeyChangesServlet(self).register(resource) KeyChangesServlet(self).register(resource)
VoipRestServlet(self).register(resource) VoipRestServlet(self).register(resource)
PushRuleRestServlet(self).register(resource) PushRuleRestServlet(self).register(resource)
VersionsRestServlet().register(resource) VersionsRestServlet(self).register(resource)
resources.update({"/_matrix/client": resource}) resources.update({"/_matrix/client": resource})

View file

@ -20,6 +20,7 @@ from __future__ import print_function
# This file can't be called email.py because if it is, we cannot: # This file can't be called email.py because if it is, we cannot:
import email.utils import email.utils
import os import os
from enum import Enum
import pkg_resources import pkg_resources
@ -74,19 +75,48 @@ class EmailConfig(Config):
"renew_at" "renew_at"
) )
email_trust_identity_server_for_password_resets = email_config.get( self.threepid_behaviour_email = (
"trust_identity_server_for_password_resets", False # Have Synapse handle the email sending if account_threepid_delegates.email
# is not defined
# msisdn is currently always remote while Synapse does not support any method of
# sending SMS messages
ThreepidBehaviour.REMOTE
if self.account_threepid_delegate_email
else ThreepidBehaviour.LOCAL
) )
self.email_password_reset_behaviour = ( # Prior to Synapse v1.4.0, there was another option that defined whether Synapse would
"remote" if email_trust_identity_server_for_password_resets else "local" # use an identity server to password reset tokens on its behalf. We now warn the user
# 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 (
not self.account_threepid_delegate_email
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:
# XXX: It's a little confusing that account_threepid_delegate_email is modified
# both in RegistrationConfig and here. We should factor this bit out
self.account_threepid_delegate_email = self.trusted_third_party_id_servers[
0
]
self.using_identity_server_from_trusted_list = True
else:
raise ConfigError(
"Attempted to use an identity server from"
'"trusted_third_party_id_servers" but it is empty.'
) )
self.password_resets_were_disabled_due_to_email_config = False
if self.email_password_reset_behaviour == "local" and email_config == {}: self.local_threepid_handling_disabled_due_to_email_config = False
if (
self.threepid_behaviour_email == ThreepidBehaviour.LOCAL
and email_config == {}
):
# We cannot warn the user this has happened here # We cannot warn the user this has happened here
# Instead do so when a user attempts to reset their password # Instead do so when a user attempts to reset their password
self.password_resets_were_disabled_due_to_email_config = True self.local_threepid_handling_disabled_due_to_email_config = True
self.email_password_reset_behaviour = "off" self.threepid_behaviour_email = ThreepidBehaviour.OFF
# Get lifetime of a validation token in milliseconds # Get lifetime of a validation token in milliseconds
self.email_validation_token_lifetime = self.parse_duration( self.email_validation_token_lifetime = self.parse_duration(
@ -96,7 +126,7 @@ class EmailConfig(Config):
if ( if (
self.email_enable_notifs self.email_enable_notifs
or account_validity_renewal_enabled or account_validity_renewal_enabled
or self.email_password_reset_behaviour == "local" or self.threepid_behaviour_email == ThreepidBehaviour.LOCAL
): ):
# make sure we can import the required deps # make sure we can import the required deps
import jinja2 import jinja2
@ -106,7 +136,7 @@ class EmailConfig(Config):
jinja2 jinja2
bleach bleach
if self.email_password_reset_behaviour == "local": if self.threepid_behaviour_email == ThreepidBehaviour.LOCAL:
required = ["smtp_host", "smtp_port", "notif_from"] required = ["smtp_host", "smtp_port", "notif_from"]
missing = [] missing = []
@ -125,28 +155,45 @@ class EmailConfig(Config):
% (", ".join(missing),) % (", ".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( self.email_password_reset_template_html = email_config.get(
"password_reset_template_html", "password_reset.html" "password_reset_template_html", "password_reset.html"
) )
self.email_password_reset_template_text = email_config.get( self.email_password_reset_template_text = email_config.get(
"password_reset_template_text", "password_reset.txt" "password_reset_template_text", "password_reset.txt"
) )
self.email_registration_template_html = email_config.get(
"registration_template_html", "registration.html"
)
self.email_registration_template_text = email_config.get(
"registration_template_text", "registration.txt"
)
self.email_password_reset_template_failure_html = email_config.get( self.email_password_reset_template_failure_html = email_config.get(
"password_reset_template_failure_html", "password_reset_failure.html" "password_reset_template_failure_html", "password_reset_failure.html"
) )
# This template does not support any replaceable variables, so we will self.email_registration_template_failure_html = email_config.get(
# read it from the disk once during setup "registration_template_failure_html", "registration_failure.html"
)
# These templates do not support any placeholder variables, so we
# will read them from disk once during setup
email_password_reset_template_success_html = email_config.get( email_password_reset_template_success_html = email_config.get(
"password_reset_template_success_html", "password_reset_success.html" "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 # Check templates exist
for f in [ for f in [
self.email_password_reset_template_html, self.email_password_reset_template_html,
self.email_password_reset_template_text, self.email_password_reset_template_text,
self.email_registration_template_html,
self.email_registration_template_text,
self.email_password_reset_template_failure_html, self.email_password_reset_template_failure_html,
email_password_reset_template_success_html, email_password_reset_template_success_html,
email_registration_template_success_html,
]: ]:
p = os.path.join(self.email_template_dir, f) p = os.path.join(self.email_template_dir, f)
if not os.path.isfile(p): if not os.path.isfile(p):
@ -156,9 +203,15 @@ class EmailConfig(Config):
filepath = os.path.join( filepath = os.path.join(
self.email_template_dir, email_password_reset_template_success_html 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(
filepath, "email.password_reset_template_success_html" 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: if self.email_enable_notifs:
required = [ required = [
@ -239,19 +292,6 @@ class EmailConfig(Config):
# # # #
# riot_base_url: "http://localhost/riot" # 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 # # Configure the time that a validation email or text message code
# # will expire after sending # # will expire after sending
# # # #
@ -283,9 +323,35 @@ class EmailConfig(Config):
# #password_reset_template_html: password_reset.html # #password_reset_template_html: password_reset.html
# #password_reset_template_text: password_reset.txt # #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 # # Templates for password reset success and failure pages that a user
# # will see after attempting to reset their password # # will see after attempting to reset their password
# # # #
# #password_reset_template_success_html: password_reset_success.html # #password_reset_template_success_html: password_reset_success.html
# #password_reset_template_failure_html: password_reset_failure.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
""" """
class ThreepidBehaviour(Enum):
"""
Enum to define the behaviour of Synapse with regards to when it contacts an identity
server for 3pid registration and password resets
REMOTE = use an external server to send tokens
LOCAL = send tokens ourselves
OFF = disable registration via 3pid and password resets
"""
REMOTE = "remote"
LOCAL = "local"
OFF = "off"

View file

@ -99,6 +99,10 @@ class RegistrationConfig(Config):
self.trusted_third_party_id_servers = config.get( self.trusted_third_party_id_servers = config.get(
"trusted_third_party_id_servers", ["matrix.org", "vector.im"] "trusted_third_party_id_servers", ["matrix.org", "vector.im"]
) )
account_threepid_delegates = config.get("account_threepid_delegates") or {}
self.account_threepid_delegate_email = account_threepid_delegates.get("email")
self.account_threepid_delegate_msisdn = account_threepid_delegates.get("msisdn")
self.default_identity_server = config.get("default_identity_server") self.default_identity_server = config.get("default_identity_server")
self.allow_guest_access = config.get("allow_guest_access", False) self.allow_guest_access = config.get("allow_guest_access", False)
@ -257,10 +261,42 @@ class RegistrationConfig(Config):
# Also defines the ID server which will be called when an account is # Also defines the ID server which will be called when an account is
# deactivated (one will be picked arbitrarily). # deactivated (one will be picked arbitrarily).
# #
# Note: This option is deprecated. Since v0.99.4, Synapse has tracked which identity
# server a 3PID has been bound to. For 3PIDs bound before then, Synapse runs a
# background migration script, informing itself that the identity server all of its
# 3PIDs have been bound to is likely one of the below.
#
# As of Synapse v1.4.0, all other functionality of this option has been deprecated, and
# it is now solely used for the purposes of the background migration script, and can be
# removed once it has run.
#trusted_third_party_id_servers: #trusted_third_party_id_servers:
# - matrix.org # - matrix.org
# - vector.im # - vector.im
# Handle threepid (email/phone etc) registration and password resets through a set of
# *trusted* identity servers. Note that this allows the configured identity server to
# reset passwords for accounts!
#
# Be aware that if `email` is not set, and SMTP options have not been
# configured in the email config block, registration and user password resets via
# email will be globally disabled.
#
# Additionally, if `msisdn` is not set, registration and password resets via msisdn
# will be disabled regardless. This is due to Synapse currently not supporting any
# method of sending SMS messages on its own.
#
# To enable using an identity server for operations regarding a particular third-party
# identifier type, set the value to the URL of that identity server as shown in the
# examples below.
#
# Servers handling the these requests must answer the `/requestToken` endpoints defined
# by the Matrix Identity Service API specification:
# https://matrix.org/docs/spec/identity_service/latest
#
account_threepid_delegates:
#email: https://example.com # Delegate email sending to matrix.org
#msisdn: http://localhost:8090 # Delegate SMS sending to this local process
# Users who register on this homeserver will automatically be joined # Users who register on this homeserver will automatically be joined
# to these rooms # to these rooms
# #

View file

@ -38,6 +38,7 @@ logger = logging.getLogger(__name__)
class AccountValidityHandler(object): class AccountValidityHandler(object):
def __init__(self, hs): def __init__(self, hs):
self.hs = hs self.hs = hs
self.config = hs.config
self.store = self.hs.get_datastore() self.store = self.hs.get_datastore()
self.sendmail = self.hs.get_sendmail() self.sendmail = self.hs.get_sendmail()
self.clock = self.hs.get_clock() self.clock = self.hs.get_clock()
@ -62,9 +63,14 @@ class AccountValidityHandler(object):
self._raw_from = email.utils.parseaddr(self._from_string)[1] self._raw_from = email.utils.parseaddr(self._from_string)[1]
self._template_html, self._template_text = load_jinja2_templates( self._template_html, self._template_text = load_jinja2_templates(
config=self.hs.config, self.config.email_template_dir,
template_html_name=self.hs.config.email_expiry_template_html, [
template_text_name=self.hs.config.email_expiry_template_text, self.config.email_expiry_template_html,
self.config.email_expiry_template_text,
],
apply_format_ts_filter=True,
apply_mxc_to_http_filter=True,
public_baseurl=self.config.public_baseurl,
) )
# Check the renewal emails to send and send them every 30min. # Check the renewal emails to send and send them every 30min.

View file

@ -38,6 +38,7 @@ from synapse.api.errors import (
UserDeactivatedError, UserDeactivatedError,
) )
from synapse.api.ratelimiting import Ratelimiter from synapse.api.ratelimiting import Ratelimiter
from synapse.config.emailconfig import ThreepidBehaviour
from synapse.logging.context import defer_to_thread from synapse.logging.context import defer_to_thread
from synapse.module_api import ModuleApi from synapse.module_api import ModuleApi
from synapse.types import UserID from synapse.types import UserID
@ -158,7 +159,7 @@ class AuthHandler(BaseHandler):
return params return params
@defer.inlineCallbacks @defer.inlineCallbacks
def check_auth(self, flows, clientdict, clientip, password_servlet=False): def check_auth(self, flows, clientdict, clientip):
""" """
Takes a dictionary sent by the client in the login / registration Takes a dictionary sent by the client in the login / registration
protocol and handles the User-Interactive Auth flow. protocol and handles the User-Interactive Auth flow.
@ -182,16 +183,6 @@ class AuthHandler(BaseHandler):
clientip (str): The IP address of the client. clientip (str): The IP address of the client.
password_servlet (bool): Whether the request originated from
PasswordRestServlet.
XXX: This is a temporary hack to distinguish between checking
for threepid validations locally (in the case of password
resets) and using the identity server (in the case of binding
a 3PID during registration). Once we start using the
homeserver for both tasks, this distinction will no longer be
necessary.
Returns: Returns:
defer.Deferred[dict, dict, str]: a deferred tuple of defer.Deferred[dict, dict, str]: a deferred tuple of
(creds, params, session_id). (creds, params, session_id).
@ -247,9 +238,7 @@ class AuthHandler(BaseHandler):
if "type" in authdict: if "type" in authdict:
login_type = authdict["type"] login_type = authdict["type"]
try: try:
result = yield self._check_auth_dict( result = yield self._check_auth_dict(authdict, clientip)
authdict, clientip, password_servlet=password_servlet
)
if result: if result:
creds[login_type] = result creds[login_type] = result
self._save_session(session) self._save_session(session)
@ -356,7 +345,7 @@ class AuthHandler(BaseHandler):
return sess.setdefault("serverdict", {}).get(key, default) return sess.setdefault("serverdict", {}).get(key, default)
@defer.inlineCallbacks @defer.inlineCallbacks
def _check_auth_dict(self, authdict, clientip, password_servlet=False): def _check_auth_dict(self, authdict, clientip):
"""Attempt to validate the auth dict provided by a client """Attempt to validate the auth dict provided by a client
Args: Args:
@ -374,11 +363,7 @@ class AuthHandler(BaseHandler):
login_type = authdict["type"] login_type = authdict["type"]
checker = self.checkers.get(login_type) checker = self.checkers.get(login_type)
if checker is not None: if checker is not None:
# XXX: Temporary workaround for having Synapse handle password resets res = yield checker(authdict, clientip=clientip)
# See AuthHandler.check_auth for further details
res = yield checker(
authdict, clientip=clientip, password_servlet=password_servlet
)
return res return res
# build a v1-login-style dict out of the authdict and fall back to the # build a v1-login-style dict out of the authdict and fall back to the
@ -449,7 +434,7 @@ class AuthHandler(BaseHandler):
return defer.succeed(True) return defer.succeed(True)
@defer.inlineCallbacks @defer.inlineCallbacks
def _check_threepid(self, medium, authdict, password_servlet=False, **kwargs): def _check_threepid(self, medium, authdict, **kwargs):
if "threepid_creds" not in authdict: if "threepid_creds" not in authdict:
raise LoginError(400, "Missing threepid_creds", Codes.MISSING_PARAM) raise LoginError(400, "Missing threepid_creds", Codes.MISSING_PARAM)
@ -458,12 +443,9 @@ class AuthHandler(BaseHandler):
identity_handler = self.hs.get_handlers().identity_handler identity_handler = self.hs.get_handlers().identity_handler
logger.info("Getting validated threepid. threepidcreds: %r", (threepid_creds,)) logger.info("Getting validated threepid. threepidcreds: %r", (threepid_creds,))
if ( if self.hs.config.threepid_behaviour_email == ThreepidBehaviour.REMOTE:
not password_servlet
or self.hs.config.email_password_reset_behaviour == "remote"
):
threepid = yield identity_handler.threepid_from_creds(threepid_creds) threepid = yield identity_handler.threepid_from_creds(threepid_creds)
elif self.hs.config.email_password_reset_behaviour == "local": elif self.hs.config.threepid_behaviour_email == ThreepidBehaviour.LOCAL:
row = yield self.store.get_threepid_validation_session( row = yield self.store.get_threepid_validation_session(
medium, medium,
threepid_creds["client_secret"], threepid_creds["client_secret"],

View file

@ -29,6 +29,7 @@ from synapse.api.errors import (
HttpResponseException, HttpResponseException,
SynapseError, SynapseError,
) )
from synapse.util.stringutils import random_string
from ._base import BaseHandler from ._base import BaseHandler
@ -41,25 +42,7 @@ class IdentityHandler(BaseHandler):
self.http_client = hs.get_simple_http_client() self.http_client = hs.get_simple_http_client()
self.federation_http_client = hs.get_http_client() self.federation_http_client = hs.get_http_client()
self.hs = hs
self.trusted_id_servers = set(hs.config.trusted_third_party_id_servers)
self.trust_any_id_server_just_for_testing_do_not_use = (
hs.config.use_insecure_ssl_client_just_for_testing_do_not_use
)
def _should_trust_id_server(self, id_server):
if id_server not in self.trusted_id_servers:
if self.trust_any_id_server_just_for_testing_do_not_use:
logger.warn(
"Trusting untrustworthy ID server %r even though it isn't"
" in the trusted id list for testing because"
" 'use_insecure_ssl_client_just_for_testing_do_not_use'"
" is set in the config",
id_server,
)
else:
return False
return True
def _extract_items_from_creds_dict(self, creds): def _extract_items_from_creds_dict(self, creds):
""" """
@ -132,13 +115,6 @@ class IdentityHandler(BaseHandler):
"/_matrix/identity/api/v1/3pid/getValidated3pid", "/_matrix/identity/api/v1/3pid/getValidated3pid",
) )
if not self._should_trust_id_server(id_server):
logger.warn(
"%s is not a trusted ID server: rejecting 3pid " + "credentials",
id_server,
)
return None
try: try:
data = yield self.http_client.get_json(url, query_params) data = yield self.http_client.get_json(url, query_params)
return data if "medium" in data else None return data if "medium" in data else None
@ -305,28 +281,122 @@ class IdentityHandler(BaseHandler):
return changed 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 @defer.inlineCallbacks
def requestEmailToken( def requestEmailToken(
self, id_server, email, client_secret, send_attempt, next_link=None self, id_server, email, client_secret, send_attempt, next_link=None
): ):
if not self._should_trust_id_server(id_server): """
raise SynapseError( Request an external server send an email on our behalf for the purposes of threepid
400, "Untrusted ID server '%s'" % id_server, Codes.SERVER_NOT_TRUSTED validation.
)
Args:
id_server (str): The identity server to proxy to
email (str): The email to send the message to
client_secret (str): The unique client_secret sends by the user
send_attempt (int): Which attempt this is
next_link: A link to redirect the user to once they submit the token
Returns:
The json response body from the server
"""
params = { params = {
"email": email, "email": email,
"client_secret": client_secret, "client_secret": client_secret,
"send_attempt": send_attempt, "send_attempt": send_attempt,
} }
if next_link: if next_link:
params.update({"next_link": next_link}) params["next_link"] = next_link
if self.hs.config.using_identity_server_from_trusted_list:
# Warn that a deprecated config option is in use
logger.warn(
'The config option "trust_identity_server_for_password_resets" '
'has been replaced by "account_threepid_delegate". '
"Please consult the sample config at docs/sample_config.yaml for "
"details and update your config file."
)
try: try:
data = yield self.http_client.post_json_get_json( data = yield self.http_client.post_json_get_json(
"https://%s%s" id_server + "/_matrix/identity/api/v1/validate/email/requestToken",
% (id_server, "/_matrix/identity/api/v1/validate/email/requestToken"),
params, params,
) )
return data return data
@ -336,25 +406,49 @@ class IdentityHandler(BaseHandler):
@defer.inlineCallbacks @defer.inlineCallbacks
def requestMsisdnToken( def requestMsisdnToken(
self, id_server, country, phone_number, client_secret, send_attempt, **kwargs self,
id_server,
country,
phone_number,
client_secret,
send_attempt,
next_link=None,
): ):
if not self._should_trust_id_server(id_server): """
raise SynapseError( Request an external server send an SMS message on our behalf for the purposes of
400, "Untrusted ID server '%s'" % id_server, Codes.SERVER_NOT_TRUSTED threepid validation.
) Args:
id_server (str): The identity server to proxy to
country (str): The country code of the phone number
phone_number (str): The number to send the message to
client_secret (str): The unique client_secret sends by the user
send_attempt (int): Which attempt this is
next_link: A link to redirect the user to once they submit the token
Returns:
The json response body from the server
"""
params = { params = {
"country": country, "country": country,
"phone_number": phone_number, "phone_number": phone_number,
"client_secret": client_secret, "client_secret": client_secret,
"send_attempt": send_attempt, "send_attempt": send_attempt,
} }
params.update(kwargs) if next_link:
params["next_link"] = next_link
if self.hs.config.using_identity_server_from_trusted_list:
# Warn that a deprecated config option is in use
logger.warn(
'The config option "trust_identity_server_for_password_resets" '
'has been replaced by "account_threepid_delegate". '
"Please consult the sample config at docs/sample_config.yaml for "
"details and update your config file."
)
try: try:
data = yield self.http_client.post_json_get_json( data = yield self.http_client.post_json_get_json(
"https://%s%s" id_server + "/_matrix/identity/api/v1/validate/msisdn/requestToken",
% (id_server, "/_matrix/identity/api/v1/validate/msisdn/requestToken"),
params, params,
) )
return data return data

View file

@ -131,14 +131,11 @@ class Mailer(object):
email_address (str): Email address we're sending the password email_address (str): Email address we're sending the password
reset to reset to
token (str): Unique token generated by the server to verify 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 client_secret (str): Unique token generated by the client to
group together multiple email sending attempts group together multiple email sending attempts
sid (str): The generated session ID sid (str): The generated session ID
""" """
if email.utils.parseaddr(email_address)[1] == "":
raise RuntimeError("Invalid 'to' email address")
link = ( link = (
self.hs.config.public_baseurl self.hs.config.public_baseurl
+ "_matrix/client/unstable/password_reset/email/submit_token" + "_matrix/client/unstable/password_reset/email/submit_token"
@ -149,7 +146,34 @@ class Mailer(object):
yield self.send_email( yield self.send_email(
email_address, 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"
"?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, template_vars,
) )
@ -605,25 +629,50 @@ def format_ts_filter(value, format):
return time.strftime(format, time.localtime(value / 1000)) return time.strftime(format, time.localtime(value / 1000))
def load_jinja2_templates(config, template_html_name, template_text_name): def load_jinja2_templates(
"""Load the jinja2 email templates from disk template_dir,
template_filenames,
apply_format_ts_filter=False,
apply_mxc_to_http_filter=False,
public_baseurl=None,
):
"""Loads and returns one or more jinja2 templates and applies optional filters
Args:
template_dir (str): The directory where templates are stored
template_filenames (list[str]): A list of template filenames
apply_format_ts_filter (bool): Whether to apply a template filter that formats
timestamps
apply_mxc_to_http_filter (bool): Whether to apply a template filter that converts
mxc urls to http urls
public_baseurl (str|None): The public baseurl of the server. Required for
apply_mxc_to_http_filter to be enabled
Returns: Returns:
(template_html, template_text) A list of jinja2 templates corresponding to the given list of filenames,
with order preserved
""" """
logger.info("loading email templates from '%s'", config.email_template_dir) logger.info(
loader = jinja2.FileSystemLoader(config.email_template_dir) "loading email templates %s from '%s'", template_filenames, template_dir
)
loader = jinja2.FileSystemLoader(template_dir)
env = jinja2.Environment(loader=loader) env = jinja2.Environment(loader=loader)
if apply_format_ts_filter:
env.filters["format_ts"] = format_ts_filter env.filters["format_ts"] = format_ts_filter
env.filters["mxc_to_http"] = _create_mxc_to_http_filter(config)
template_html = env.get_template(template_html_name) if apply_mxc_to_http_filter and public_baseurl:
template_text = env.get_template(template_text_name) env.filters["mxc_to_http"] = _create_mxc_to_http_filter(public_baseurl)
return template_html, template_text templates = []
for template_filename in template_filenames:
template = env.get_template(template_filename)
templates.append(template)
return templates
def _create_mxc_to_http_filter(config): def _create_mxc_to_http_filter(public_baseurl):
def mxc_to_http_filter(value, width, height, resize_method="crop"): def mxc_to_http_filter(value, width, height, resize_method="crop"):
if value[0:6] != "mxc://": if value[0:6] != "mxc://":
return "" return ""
@ -636,7 +685,7 @@ def _create_mxc_to_http_filter(config):
params = {"width": width, "height": height, "method": resize_method} params = {"width": width, "height": height, "method": resize_method}
return "%s_matrix/media/v1/thumbnail/%s?%s%s" % ( return "%s_matrix/media/v1/thumbnail/%s?%s%s" % (
config.public_baseurl, public_baseurl,
serverAndMediaId, serverAndMediaId,
urllib.parse.urlencode(params), urllib.parse.urlencode(params),
fragment or "", fragment or "",

View file

@ -35,6 +35,7 @@ except Exception:
class PusherFactory(object): class PusherFactory(object):
def __init__(self, hs): def __init__(self, hs):
self.hs = hs self.hs = hs
self.config = hs.config
self.pusher_types = {"http": HttpPusher} self.pusher_types = {"http": HttpPusher}
@ -42,12 +43,16 @@ class PusherFactory(object):
if hs.config.email_enable_notifs: if hs.config.email_enable_notifs:
self.mailers = {} # app_name -> Mailer self.mailers = {} # app_name -> Mailer
templates = load_jinja2_templates( self.notif_template_html, self.notif_template_text = load_jinja2_templates(
config=hs.config, self.config.email_template_dir,
template_html_name=hs.config.email_notif_template_html, [
template_text_name=hs.config.email_notif_template_text, self.config.email_notif_template_html,
self.config.email_notif_template_text,
],
apply_format_ts_filter=True,
apply_mxc_to_http_filter=True,
public_baseurl=self.config.public_baseurl,
) )
self.notif_template_html, self.notif_template_text = templates
self.pusher_types["email"] = self._create_email_pusher self.pusher_types["email"] = self._create_email_pusher
@ -78,6 +83,6 @@ class PusherFactory(object):
if "data" in pusherdict and "brand" in pusherdict["data"]: if "data" in pusherdict and "brand" in pusherdict["data"]:
app_name = pusherdict["data"]["brand"] app_name = pusherdict["data"]["brand"]
else: else:
app_name = self.hs.config.email_app_name app_name = self.config.email_app_name
return app_name return app_name

View file

@ -4,6 +4,6 @@
<a href="{{ link }}">{{ link }}</a> <a href="{{ link }}">{{ link }}</a>
<p>If this was not you, please disregard this email and contact your server administrator. Thank you.</p> <p>If this was not you, <strong>do not</strong> click the link above and instead contact your server administrator. Thank you.</p>
</body> </body>
</html> </html>

View file

@ -3,5 +3,5 @@ was you, please click the link below to confirm resetting your password:
{{ link }} {{ link }}
If this was not you, please disregard this email and contact your server If this was not you, DO NOT click the link above and instead contact your
administrator. Thank you. server administrator. Thank you.

View file

@ -1,6 +1,8 @@
<html> <html>
<head></head> <head></head>
<body> <body>
<p>{{ failure_reason }}. Your password has not been reset.</p> <p>The request failed for the following reason: {{ failure_reason }}.</p>
<p>Your password has not been reset.</p>
</body> </body>
</html> </html>

View file

@ -0,0 +1,11 @@
<html>
<body>
<p>You have asked us to register this email with a new Matrix account. If this was you, please click the link below to confirm your email address:</p>
<a href="{{ link }}">Verify Your Email Address</a>
<p>If this was not you, you can safely disregard this email.</p>
<p>Thank you.</p>
</body>
</html>

View file

@ -0,0 +1,10 @@
Hello there,
You have asked us to register this email with a new Matrix account. If this
was you, please click the link below to confirm your email address:
{{ link }}
If this was not you, you can safely disregard this email.
Thank you.

View file

@ -0,0 +1,6 @@
<html>
<head></head>
<body>
<p>Validation failed for the following reason: {{ failure_reason }}.</p>
</body>
</html>

View file

@ -0,0 +1,6 @@
<html>
<head></head>
<body>
<p>Your email has now been validated, please return to your client. You may now close this window.</p>
</body>
</html>

View file

@ -73,7 +73,7 @@ class ClientRestResource(JsonResource):
@staticmethod @staticmethod
def register_servlets(client_resource, hs): def register_servlets(client_resource, hs):
versions.register_servlets(client_resource) versions.register_servlets(hs, client_resource)
# Deprecated in r0 # Deprecated in r0
initial_sync.register_servlets(hs, client_resource) initial_sync.register_servlets(hs, client_resource)

View file

@ -37,6 +37,7 @@ def client_patterns(path_regex, releases=(0,), unstable=True, v1=False):
SRE_Pattern SRE_Pattern
""" """
patterns = [] patterns = []
if unstable: if unstable:
unstable_prefix = CLIENT_API_PREFIX + "/unstable" unstable_prefix = CLIENT_API_PREFIX + "/unstable"
patterns.append(re.compile("^" + unstable_prefix + path_regex)) patterns.append(re.compile("^" + unstable_prefix + path_regex))
@ -46,6 +47,7 @@ def client_patterns(path_regex, releases=(0,), unstable=True, v1=False):
for release in releases: for release in releases:
new_prefix = CLIENT_API_PREFIX + "/r%d" % (release,) new_prefix = CLIENT_API_PREFIX + "/r%d" % (release,)
patterns.append(re.compile("^" + new_prefix + path_regex)) patterns.append(re.compile("^" + new_prefix + path_regex))
return patterns return patterns

View file

@ -18,12 +18,11 @@ import logging
from six.moves import http_client from six.moves import http_client
import jinja2
from twisted.internet import defer from twisted.internet import defer
from synapse.api.constants import LoginType from synapse.api.constants import LoginType
from synapse.api.errors import Codes, SynapseError, ThreepidValidationError from synapse.api.errors import Codes, SynapseError, ThreepidValidationError
from synapse.config.emailconfig import ThreepidBehaviour
from synapse.http.server import finish_request from synapse.http.server import finish_request
from synapse.http.servlet import ( from synapse.http.servlet import (
RestServlet, RestServlet,
@ -31,8 +30,8 @@ from synapse.http.servlet import (
parse_json_object_from_request, parse_json_object_from_request,
parse_string, parse_string,
) )
from synapse.push.mailer import Mailer, load_jinja2_templates
from synapse.util.msisdn import phone_number_to_msisdn 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 synapse.util.threepids import check_3pid_allowed
from ._base import client_patterns, interactive_auth_handler from ._base import client_patterns, interactive_auth_handler
@ -50,25 +49,28 @@ class EmailPasswordRequestTokenRestServlet(RestServlet):
self.config = hs.config self.config = hs.config
self.identity_handler = hs.get_handlers().identity_handler self.identity_handler = hs.get_handlers().identity_handler
if self.config.email_password_reset_behaviour == "local": if self.config.threepid_behaviour_email == ThreepidBehaviour.LOCAL:
from synapse.push.mailer import Mailer, load_jinja2_templates template_html, template_text = load_jinja2_templates(
self.config.email_template_dir,
templates = load_jinja2_templates( [
config=hs.config, self.config.email_password_reset_template_html,
template_html_name=hs.config.email_password_reset_template_html, self.config.email_password_reset_template_text,
template_text_name=hs.config.email_password_reset_template_text, ],
apply_format_ts_filter=True,
apply_mxc_to_http_filter=True,
public_baseurl=self.config.public_baseurl,
) )
self.mailer = Mailer( self.mailer = Mailer(
hs=self.hs, hs=self.hs,
app_name=self.config.email_app_name, app_name=self.config.email_app_name,
template_html=templates[0], template_html=template_html,
template_text=templates[1], template_text=template_text,
) )
@defer.inlineCallbacks @defer.inlineCallbacks
def on_POST(self, request): def on_POST(self, request):
if self.config.email_password_reset_behaviour == "off": if self.config.threepid_behaviour_email == ThreepidBehaviour.OFF:
if self.config.password_resets_were_disabled_due_to_email_config: if self.config.local_threepid_handling_disabled_due_to_email_config:
logger.warn( logger.warn(
"User password resets have been disabled due to lack of email config" "User password resets have been disabled due to lack of email config"
) )
@ -93,25 +95,39 @@ class EmailPasswordRequestTokenRestServlet(RestServlet):
Codes.THREEPID_DENIED, Codes.THREEPID_DENIED,
) )
existingUid = yield self.hs.get_datastore().get_user_id_by_threepid( existing_user_id = yield self.hs.get_datastore().get_user_id_by_threepid(
"email", email "email", email
) )
if existingUid is None: if existing_user_id is None:
raise SynapseError(400, "Email not found", Codes.THREEPID_NOT_FOUND) raise SynapseError(400, "Email not found", Codes.THREEPID_NOT_FOUND)
if self.config.email_password_reset_behaviour == "remote": if self.config.threepid_behaviour_email == ThreepidBehaviour.REMOTE:
if "id_server" not in body: # Have the configured identity server handle the request
raise SynapseError(400, "Missing 'id_server' param in body") if not self.hs.config.account_threepid_delegate_email:
logger.warn(
"No upstream email account_threepid_delegate configured on the server to "
"handle this request"
)
raise SynapseError(
400, "Password reset by email is not supported on this homeserver"
)
# Have the identity server handle the password reset flow
ret = yield self.identity_handler.requestEmailToken( ret = yield self.identity_handler.requestEmailToken(
body["id_server"], email, client_secret, send_attempt, next_link self.hs.config.account_threepid_delegate_email,
email,
client_secret,
send_attempt,
next_link,
) )
else: else:
# Send password reset emails from Synapse # Send password reset emails from Synapse
sid = yield self.send_password_reset( sid = yield self.identity_handler.send_threepid_validation(
email, client_secret, send_attempt, next_link email,
client_secret,
send_attempt,
self.mailer.send_password_reset_mail,
next_link,
) )
# Wrap the session id in a JSON object # Wrap the session id in a JSON object
@ -119,74 +135,6 @@ class EmailPasswordRequestTokenRestServlet(RestServlet):
return 200, ret return 200, ret
@defer.inlineCallbacks
def send_password_reset(self, email, client_secret, send_attempt, next_link=None):
"""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
session = yield self.datastore.get_threepid_validation_session(
"email", client_secret, address=email, 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 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.email_validation_token_lifetime
)
yield self.datastore.start_or_continue_validation_session(
"email",
email,
session_id,
client_secret,
send_attempt,
next_link,
token,
token_expires,
)
return session_id
class MsisdnPasswordRequestTokenRestServlet(RestServlet): class MsisdnPasswordRequestTokenRestServlet(RestServlet):
PATTERNS = client_patterns("/account/password/msisdn/requestToken$") PATTERNS = client_patterns("/account/password/msisdn/requestToken$")
@ -202,11 +150,15 @@ class MsisdnPasswordRequestTokenRestServlet(RestServlet):
body = parse_json_object_from_request(request) body = parse_json_object_from_request(request)
assert_params_in_dict( assert_params_in_dict(
body, body, ["client_secret", "country", "phone_number", "send_attempt"]
["id_server", "client_secret", "country", "phone_number", "send_attempt"],
) )
client_secret = body["client_secret"]
country = body["country"]
phone_number = body["phone_number"]
send_attempt = body["send_attempt"]
next_link = body.get("next_link") # Optional param
msisdn = phone_number_to_msisdn(body["country"], body["phone_number"]) msisdn = phone_number_to_msisdn(country, phone_number)
if not check_3pid_allowed(self.hs, "msisdn", msisdn): if not check_3pid_allowed(self.hs, "msisdn", msisdn):
raise SynapseError( raise SynapseError(
@ -215,12 +167,32 @@ class MsisdnPasswordRequestTokenRestServlet(RestServlet):
Codes.THREEPID_DENIED, Codes.THREEPID_DENIED,
) )
existingUid = yield self.datastore.get_user_id_by_threepid("msisdn", msisdn) existing_user_id = yield self.datastore.get_user_id_by_threepid(
"msisdn", msisdn
)
if existingUid is None: if existing_user_id is None:
raise SynapseError(400, "MSISDN not found", Codes.THREEPID_NOT_FOUND) raise SynapseError(400, "MSISDN not found", Codes.THREEPID_NOT_FOUND)
ret = yield self.identity_handler.requestMsisdnToken(**body) if not self.hs.config.account_threepid_delegate_msisdn:
logger.warn(
"No upstream msisdn account_threepid_delegate configured on the server to "
"handle this request"
)
raise SynapseError(
400,
"Password reset by phone number is not supported on this homeserver",
)
ret = yield self.identity_handler.requestMsisdnToken(
self.hs.config.account_threepid_delegate_msisdn,
country,
phone_number,
client_secret,
send_attempt,
next_link,
)
return 200, ret return 200, ret
@ -241,31 +213,32 @@ class PasswordResetSubmitTokenServlet(RestServlet):
self.auth = hs.get_auth() self.auth = hs.get_auth()
self.config = hs.config self.config = hs.config
self.clock = hs.get_clock() self.clock = hs.get_clock()
self.datastore = hs.get_datastore() self.store = hs.get_datastore()
@defer.inlineCallbacks @defer.inlineCallbacks
def on_GET(self, request, medium): def on_GET(self, request, medium):
# We currently only handle threepid token submissions for email
if medium != "email": if medium != "email":
raise SynapseError( raise SynapseError(
400, "This medium is currently not supported for password resets" 400, "This medium is currently not supported for password resets"
) )
if self.config.email_password_reset_behaviour == "off": if self.config.threepid_behaviour_email == ThreepidBehaviour.OFF:
if self.config.password_resets_were_disabled_due_to_email_config: if self.config.local_threepid_handling_disabled_due_to_email_config:
logger.warn( logger.warn(
"User password resets have been disabled due to lack of email config" "Password reset emails have been disabled due to lack of an email config"
) )
raise SynapseError( raise SynapseError(
400, "Email-based password resets have been disabled on this server" 400, "Email-based password resets are disabled on this server"
) )
sid = parse_string(request, "sid") sid = parse_string(request, "sid", required=True)
client_secret = parse_string(request, "client_secret") client_secret = parse_string(request, "client_secret", required=True)
token = parse_string(request, "token") token = parse_string(request, "token", required=True)
# Attempt to validate a 3PID sesssion # Attempt to validate a 3PID session
try: try:
# Mark the session as valid # Mark the session as valid
next_link = yield self.datastore.validate_threepid_session( next_link = yield self.store.validate_threepid_session(
sid, client_secret, token, self.clock.time_msec() sid, client_secret, token, self.clock.time_msec()
) )
@ -282,38 +255,22 @@ class PasswordResetSubmitTokenServlet(RestServlet):
return None return None
# Otherwise show the success template # Otherwise show the success template
html = self.config.email_password_reset_template_success_html_content html = self.config.email_password_reset_template_success_html
request.setResponseCode(200) request.setResponseCode(200)
except ThreepidValidationError as e: except ThreepidValidationError as e:
# Show a failure page with a reason
html = self.load_jinja2_template(
self.config.email_template_dir,
self.config.email_password_reset_template_failure_html,
template_vars={"failure_reason": e.msg},
)
request.setResponseCode(e.code) request.setResponseCode(e.code)
# Show a failure page with a reason
html_template = load_jinja2_templates(
self.config.email_template_dir,
[self.config.email_password_reset_template_failure_html],
)
template_vars = {"failure_reason": e.msg}
html = html_template.render(**template_vars)
request.write(html.encode("utf-8")) request.write(html.encode("utf-8"))
finish_request(request) finish_request(request)
return None
def load_jinja2_template(self, template_dir, template_filename, template_vars):
"""Loads a jinja2 template with variables to insert
Args:
template_dir (str): The directory where templates are stored
template_filename (str): The name of the template in the template_dir
template_vars (Dict): Dictionary of keys in the template
alongside their values to insert
Returns:
str containing the contents of the rendered template
"""
loader = jinja2.FileSystemLoader(template_dir)
env = jinja2.Environment(loader=loader)
template = env.get_template(template_filename)
return template.render(**template_vars)
@defer.inlineCallbacks @defer.inlineCallbacks
def on_POST(self, request, medium): def on_POST(self, request, medium):
@ -325,7 +282,7 @@ class PasswordResetSubmitTokenServlet(RestServlet):
body = parse_json_object_from_request(request) body = parse_json_object_from_request(request)
assert_params_in_dict(body, ["sid", "client_secret", "token"]) assert_params_in_dict(body, ["sid", "client_secret", "token"])
valid, _ = yield self.datastore.validate_threepid_validation_token( valid, _ = yield self.store.validate_threepid_session(
body["sid"], body["client_secret"], body["token"], self.clock.time_msec() body["sid"], body["client_secret"], body["token"], self.clock.time_msec()
) )
response_code = 200 if valid else 400 response_code = 200 if valid else 400
@ -371,7 +328,6 @@ class PasswordRestServlet(RestServlet):
[[LoginType.EMAIL_IDENTITY], [LoginType.MSISDN]], [[LoginType.EMAIL_IDENTITY], [LoginType.MSISDN]],
body, body,
self.hs.get_ip_from_request(request), self.hs.get_ip_from_request(request),
password_servlet=True,
) )
if LoginType.EMAIL_IDENTITY in result: if LoginType.EMAIL_IDENTITY in result:
@ -454,10 +410,11 @@ class EmailThreepidRequestTokenRestServlet(RestServlet):
PATTERNS = client_patterns("/account/3pid/email/requestToken$") PATTERNS = client_patterns("/account/3pid/email/requestToken$")
def __init__(self, hs): def __init__(self, hs):
self.hs = hs
super(EmailThreepidRequestTokenRestServlet, self).__init__() super(EmailThreepidRequestTokenRestServlet, self).__init__()
self.hs = hs
self.config = hs.config
self.identity_handler = hs.get_handlers().identity_handler self.identity_handler = hs.get_handlers().identity_handler
self.datastore = self.hs.get_datastore() self.store = self.hs.get_datastore()
@defer.inlineCallbacks @defer.inlineCallbacks
def on_POST(self, request): def on_POST(self, request):
@ -465,22 +422,29 @@ class EmailThreepidRequestTokenRestServlet(RestServlet):
assert_params_in_dict( assert_params_in_dict(
body, ["id_server", "client_secret", "email", "send_attempt"] body, ["id_server", "client_secret", "email", "send_attempt"]
) )
id_server = "https://" + body["id_server"] # Assume https
client_secret = body["client_secret"]
email = body["email"]
send_attempt = body["send_attempt"]
next_link = body.get("next_link") # Optional param
if not check_3pid_allowed(self.hs, "email", body["email"]): if not check_3pid_allowed(self.hs, "email", email):
raise SynapseError( raise SynapseError(
403, 403,
"Your email domain is not authorized on this server", "Your email domain is not authorized on this server",
Codes.THREEPID_DENIED, Codes.THREEPID_DENIED,
) )
existingUid = yield self.datastore.get_user_id_by_threepid( existing_user_id = yield self.store.get_user_id_by_threepid(
"email", body["email"] "email", body["email"]
) )
if existingUid is not None: if existing_user_id is not None:
raise SynapseError(400, "Email is already in use", Codes.THREEPID_IN_USE) raise SynapseError(400, "Email is already in use", Codes.THREEPID_IN_USE)
ret = yield self.identity_handler.requestEmailToken(**body) ret = yield self.identity_handler.requestEmailToken(
id_server, email, client_secret, send_attempt, next_link
)
return 200, ret return 200, ret
@ -490,8 +454,8 @@ class MsisdnThreepidRequestTokenRestServlet(RestServlet):
def __init__(self, hs): def __init__(self, hs):
self.hs = hs self.hs = hs
super(MsisdnThreepidRequestTokenRestServlet, self).__init__() super(MsisdnThreepidRequestTokenRestServlet, self).__init__()
self.store = self.hs.get_datastore()
self.identity_handler = hs.get_handlers().identity_handler self.identity_handler = hs.get_handlers().identity_handler
self.datastore = self.hs.get_datastore()
@defer.inlineCallbacks @defer.inlineCallbacks
def on_POST(self, request): def on_POST(self, request):
@ -500,8 +464,14 @@ class MsisdnThreepidRequestTokenRestServlet(RestServlet):
body, body,
["id_server", "client_secret", "country", "phone_number", "send_attempt"], ["id_server", "client_secret", "country", "phone_number", "send_attempt"],
) )
id_server = "https://" + body["id_server"] # Assume https
client_secret = body["client_secret"]
country = body["country"]
phone_number = body["phone_number"]
send_attempt = body["send_attempt"]
next_link = body.get("next_link") # Optional param
msisdn = phone_number_to_msisdn(body["country"], body["phone_number"]) msisdn = phone_number_to_msisdn(country, phone_number)
if not check_3pid_allowed(self.hs, "msisdn", msisdn): if not check_3pid_allowed(self.hs, "msisdn", msisdn):
raise SynapseError( raise SynapseError(
@ -510,12 +480,14 @@ class MsisdnThreepidRequestTokenRestServlet(RestServlet):
Codes.THREEPID_DENIED, Codes.THREEPID_DENIED,
) )
existingUid = yield self.datastore.get_user_id_by_threepid("msisdn", msisdn) existing_user_id = yield self.store.get_user_id_by_threepid("msisdn", msisdn)
if existingUid is not None: if existing_user_id is not None:
raise SynapseError(400, "MSISDN is already in use", Codes.THREEPID_IN_USE) raise SynapseError(400, "MSISDN is already in use", Codes.THREEPID_IN_USE)
ret = yield self.identity_handler.requestMsisdnToken(**body) ret = yield self.identity_handler.requestMsisdnToken(
id_server, country, phone_number, client_secret, send_attempt, next_link
)
return 200, ret return 200, ret

View file

@ -28,16 +28,20 @@ from synapse.api.errors import (
Codes, Codes,
LimitExceededError, LimitExceededError,
SynapseError, SynapseError,
ThreepidValidationError,
UnrecognizedRequestError, UnrecognizedRequestError,
) )
from synapse.config.emailconfig import ThreepidBehaviour
from synapse.config.ratelimiting import FederationRateLimitConfig from synapse.config.ratelimiting import FederationRateLimitConfig
from synapse.config.server import is_threepid_reserved from synapse.config.server import is_threepid_reserved
from synapse.http.server import finish_request
from synapse.http.servlet import ( from synapse.http.servlet import (
RestServlet, RestServlet,
assert_params_in_dict, assert_params_in_dict,
parse_json_object_from_request, parse_json_object_from_request,
parse_string, parse_string,
) )
from synapse.push.mailer import load_jinja2_templates
from synapse.util.msisdn import phone_number_to_msisdn from synapse.util.msisdn import phone_number_to_msisdn
from synapse.util.ratelimitutils import FederationRateLimiter from synapse.util.ratelimitutils import FederationRateLimiter
from synapse.util.threepids import check_3pid_allowed from synapse.util.threepids import check_3pid_allowed
@ -70,30 +74,92 @@ class EmailRegisterRequestTokenRestServlet(RestServlet):
super(EmailRegisterRequestTokenRestServlet, self).__init__() super(EmailRegisterRequestTokenRestServlet, self).__init__()
self.hs = hs self.hs = hs
self.identity_handler = hs.get_handlers().identity_handler self.identity_handler = hs.get_handlers().identity_handler
self.config = hs.config
if self.hs.config.threepid_behaviour_email == ThreepidBehaviour.LOCAL:
from synapse.push.mailer import Mailer, load_jinja2_templates
template_html, template_text = load_jinja2_templates(
self.config.email_template_dir,
[
self.config.email_registration_template_html,
self.config.email_registration_template_text,
],
apply_format_ts_filter=True,
apply_mxc_to_http_filter=True,
public_baseurl=self.config.public_baseurl,
)
self.mailer = Mailer(
hs=self.hs,
app_name=self.config.email_app_name,
template_html=template_html,
template_text=template_text,
)
@defer.inlineCallbacks @defer.inlineCallbacks
def on_POST(self, request): def on_POST(self, request):
if self.hs.config.threepid_behaviour_email == ThreepidBehaviour.OFF:
if self.hs.config.local_threepid_handling_disabled_due_to_email_config:
logger.warn(
"Email registration has been disabled due to lack of email config"
)
raise SynapseError(
400, "Email-based registration has been disabled on this server"
)
body = parse_json_object_from_request(request) body = parse_json_object_from_request(request)
assert_params_in_dict( assert_params_in_dict(body, ["client_secret", "email", "send_attempt"])
body, ["id_server", "client_secret", "email", "send_attempt"]
)
if not check_3pid_allowed(self.hs, "email", body["email"]): # Extract params from body
client_secret = body["client_secret"]
email = body["email"]
send_attempt = body["send_attempt"]
next_link = body.get("next_link") # Optional param
if not check_3pid_allowed(self.hs, "email", email):
raise SynapseError( raise SynapseError(
403, 403,
"Your email domain is not authorized to register on this server", "Your email domain is not authorized to register on this server",
Codes.THREEPID_DENIED, Codes.THREEPID_DENIED,
) )
existingUid = yield self.hs.get_datastore().get_user_id_by_threepid( existing_user_id = yield self.hs.get_datastore().get_user_id_by_threepid(
"email", body["email"] "email", body["email"]
) )
if existingUid is not None: if existing_user_id is not None:
raise SynapseError(400, "Email is already in use", Codes.THREEPID_IN_USE) raise SynapseError(400, "Email is already in use", Codes.THREEPID_IN_USE)
ret = yield self.identity_handler.requestEmailToken(**body) if self.config.threepid_behaviour_email == ThreepidBehaviour.REMOTE:
if not self.hs.config.account_threepid_delegate_email:
logger.warn(
"No upstream email account_threepid_delegate configured on the server to "
"handle this request"
)
raise SynapseError(
400, "Registration by email is not supported on this homeserver"
)
ret = yield self.identity_handler.requestEmailToken(
self.hs.config.account_threepid_delegate_email,
email,
client_secret,
send_attempt,
next_link,
)
else:
# Send registration emails from Synapse
sid = yield self.identity_handler.send_threepid_validation(
email,
client_secret,
send_attempt,
self.mailer.send_registration_mail,
next_link,
)
# Wrap the session id in a JSON object
ret = {"sid": sid}
return 200, ret return 200, ret
@ -114,11 +180,15 @@ class MsisdnRegisterRequestTokenRestServlet(RestServlet):
body = parse_json_object_from_request(request) body = parse_json_object_from_request(request)
assert_params_in_dict( assert_params_in_dict(
body, body, ["client_secret", "country", "phone_number", "send_attempt"]
["id_server", "client_secret", "country", "phone_number", "send_attempt"],
) )
client_secret = body["client_secret"]
country = body["country"]
phone_number = body["phone_number"]
send_attempt = body["send_attempt"]
next_link = body.get("next_link") # Optional param
msisdn = phone_number_to_msisdn(body["country"], body["phone_number"]) msisdn = phone_number_to_msisdn(country, phone_number)
if not check_3pid_allowed(self.hs, "msisdn", msisdn): if not check_3pid_allowed(self.hs, "msisdn", msisdn):
raise SynapseError( raise SynapseError(
@ -127,19 +197,114 @@ class MsisdnRegisterRequestTokenRestServlet(RestServlet):
Codes.THREEPID_DENIED, Codes.THREEPID_DENIED,
) )
existingUid = yield self.hs.get_datastore().get_user_id_by_threepid( existing_user_id = yield self.hs.get_datastore().get_user_id_by_threepid(
"msisdn", msisdn "msisdn", msisdn
) )
if existingUid is not None: if existing_user_id is not None:
raise SynapseError( raise SynapseError(
400, "Phone number is already in use", Codes.THREEPID_IN_USE 400, "Phone number is already in use", Codes.THREEPID_IN_USE
) )
ret = yield self.identity_handler.requestMsisdnToken(**body) if not self.hs.config.account_threepid_delegate_msisdn:
logger.warn(
"No upstream msisdn account_threepid_delegate configured on the server to "
"handle this request"
)
raise SynapseError(
400, "Registration by phone number is not supported on this homeserver"
)
ret = yield self.identity_handler.requestMsisdnToken(
self.hs.config.account_threepid_delegate_msisdn,
country,
phone_number,
client_secret,
send_attempt,
next_link,
)
return 200, ret return 200, ret
class RegistrationSubmitTokenServlet(RestServlet):
"""Handles registration 3PID validation token submission"""
PATTERNS = client_patterns(
"/registration/(?P<medium>[^/]*)/submit_token$", releases=(), unstable=True
)
def __init__(self, hs):
"""
Args:
hs (synapse.server.HomeServer): server
"""
super(RegistrationSubmitTokenServlet, self).__init__()
self.hs = hs
self.auth = hs.get_auth()
self.config = hs.config
self.clock = hs.get_clock()
self.store = hs.get_datastore()
@defer.inlineCallbacks
def on_GET(self, request, medium):
if medium != "email":
raise SynapseError(
400, "This medium is currently not supported for registration"
)
if self.config.threepid_behaviour_email == ThreepidBehaviour.OFF:
if self.config.local_threepid_handling_disabled_due_to_email_config:
logger.warn(
"User registration via email has been disabled due to lack of email config"
)
raise SynapseError(
400, "Email-based registration is disabled on this server"
)
sid = parse_string(request, "sid", required=True)
client_secret = parse_string(request, "client_secret", required=True)
token = parse_string(request, "token", required=True)
# Attempt to validate a 3PID session
try:
# Mark the session as valid
next_link = yield self.store.validate_threepid_session(
sid, client_secret, token, self.clock.time_msec()
)
# Perform a 302 redirect if next_link is set
if next_link:
if next_link.startswith("file:///"):
logger.warn(
"Not redirecting to next_link as it is a local file: address"
)
else:
request.setResponseCode(302)
request.setHeader("Location", next_link)
finish_request(request)
return None
# Otherwise show the success template
html = self.config.email_registration_template_success_html_content
request.setResponseCode(200)
except ThreepidValidationError as e:
# Show a failure page with a reason
request.setResponseCode(e.code)
# Show a failure page with a reason
html_template = load_jinja2_templates(
self.config.email_template_dir,
[self.config.email_registration_template_failure_html],
)
template_vars = {"failure_reason": e.msg}
html = html_template.render(**template_vars)
request.write(html.encode("utf-8"))
finish_request(request)
class UsernameAvailabilityRestServlet(RestServlet): class UsernameAvailabilityRestServlet(RestServlet):
PATTERNS = client_patterns("/register/available") PATTERNS = client_patterns("/register/available")
@ -438,11 +603,11 @@ class RegisterRestServlet(RestServlet):
medium = auth_result[login_type]["medium"] medium = auth_result[login_type]["medium"]
address = auth_result[login_type]["address"] address = auth_result[login_type]["address"]
existingUid = yield self.store.get_user_id_by_threepid( existing_user_id = yield self.store.get_user_id_by_threepid(
medium, address medium, address
) )
if existingUid is not None: if existing_user_id is not None:
raise SynapseError( raise SynapseError(
400, 400,
"%s is already in use" % medium, "%s is already in use" % medium,
@ -550,4 +715,5 @@ def register_servlets(hs, http_server):
EmailRegisterRequestTokenRestServlet(hs).register(http_server) EmailRegisterRequestTokenRestServlet(hs).register(http_server)
MsisdnRegisterRequestTokenRestServlet(hs).register(http_server) MsisdnRegisterRequestTokenRestServlet(hs).register(http_server)
UsernameAvailabilityRestServlet(hs).register(http_server) UsernameAvailabilityRestServlet(hs).register(http_server)
RegistrationSubmitTokenServlet(hs).register(http_server)
RegisterRestServlet(hs).register(http_server) RegisterRestServlet(hs).register(http_server)

View file

@ -24,6 +24,10 @@ logger = logging.getLogger(__name__)
class VersionsRestServlet(RestServlet): class VersionsRestServlet(RestServlet):
PATTERNS = [re.compile("^/_matrix/client/versions$")] PATTERNS = [re.compile("^/_matrix/client/versions$")]
def __init__(self, hs):
super(VersionsRestServlet, self).__init__()
self.config = hs.config
def on_GET(self, request): def on_GET(self, request):
return ( return (
200, 200,
@ -49,5 +53,5 @@ class VersionsRestServlet(RestServlet):
) )
def register_servlets(http_server): def register_servlets(hs, http_server):
VersionsRestServlet().register(http_server) VersionsRestServlet(hs).register(http_server)