Merge branch 'develop' of github.com:matrix-org/synapse into erikj/cleanup_user_ips_2

This commit is contained in:
Erik Johnston 2019-09-25 17:53:13 +01:00
commit 4fb3c129aa
42 changed files with 1318 additions and 496 deletions

1
changelog.d/5884.feature Normal file
View File

@ -0,0 +1 @@
Enable cleaning up extremities with dummy events by default to prevent undue build up of forward extremities.

1
changelog.d/5972.misc Normal file
View File

@ -0,0 +1 @@
Add m.require_identity_server flag to /version's unstable_features.

1
changelog.d/5974.feature Normal file
View File

@ -0,0 +1 @@
Add m.id_access_token to unstable_features in /versions as per MSC2264.

1
changelog.d/6000.feature Normal file
View File

@ -0,0 +1 @@
Apply the federation blacklist to requests to identity servers.

1
changelog.d/6037.feature Normal file
View File

@ -0,0 +1 @@
Make the process for mapping SAML2 users to matrix IDs more flexible.

1
changelog.d/6043.feature Normal file
View File

@ -0,0 +1 @@
Implement new Client Server API endpoints `/account/3pid/add` and `/account/3pid/bind` as per [MSC2290](https://github.com/matrix-org/matrix-doc/pull/2290).

1
changelog.d/6044.feature Normal file
View File

@ -0,0 +1 @@
Add an unstable feature flag for separate add/bind 3pid APIs.

1
changelog.d/6064.misc Normal file
View File

@ -0,0 +1 @@
Clean up the sample config for SAML authentication.

1
changelog.d/6069.bugfix Normal file
View File

@ -0,0 +1 @@
Fix a bug which caused SAML attribute maps to be overridden by defaults.

1
changelog.d/6078.feature Normal file
View File

@ -0,0 +1 @@
Add `POST /add_threepid/msisdn/submit_token` endpoint for proxying submitToken on an account_threepid_handler.

1
changelog.d/6079.feature Normal file
View File

@ -0,0 +1 @@
Add `submit_url` response parameter to `*/msisdn/requestToken` endpoints.

1
changelog.d/6092.bugfix Normal file
View File

@ -0,0 +1 @@
Fix the logged number of updated items for the users_set_deactivated_flag background update.

1
changelog.d/6097.bugfix Normal file
View File

@ -0,0 +1 @@
Add sid to next_link for email validation.

1
changelog.d/6099.misc Normal file
View File

@ -0,0 +1 @@
Remove unused parameter to get_user_id_by_threepid.

1
changelog.d/6104.bugfix Normal file
View File

@ -0,0 +1 @@
Threepid validity checks on msisdns should not be dependent on 'threepid_behaviour_email'.

1
changelog.d/6105.misc Normal file
View File

@ -0,0 +1 @@
Refactor the user-interactive auth handling.

1
changelog.d/6106.misc Normal file
View File

@ -0,0 +1 @@
Refactor code for calculating registration flows.

1
changelog.d/6107.bugfix Normal file
View File

@ -0,0 +1 @@
Ensure that servers which are not configured to support email address verification do not offer it in the registration flows.

View File

@ -110,6 +110,9 @@ pid_file: DATADIR/homeserver.pid
# blacklist IP address CIDR ranges. If this option is not specified, or # blacklist IP address CIDR ranges. If this option is not specified, or
# specified with an empty list, no ip range blacklist will be enforced. # specified with an empty list, no ip range blacklist will be enforced.
# #
# As of Synapse v1.4.0 this option also affects any outbound requests to identity
# servers provided by user input.
#
# (0.0.0.0 and :: are always blacklisted, whether or not they are explicitly # (0.0.0.0 and :: are always blacklisted, whether or not they are explicitly
# listed here, since they correspond to unroutable addresses.) # listed here, since they correspond to unroutable addresses.)
# #
@ -943,6 +946,8 @@ uploads_path: "DATADIR/uploads"
# by the Matrix Identity Service API specification: # by the Matrix Identity Service API specification:
# https://matrix.org/docs/spec/identity_service/latest # https://matrix.org/docs/spec/identity_service/latest
# #
# If a delegate is specified, the config option public_baseurl must also be filled out.
#
account_threepid_delegates: account_threepid_delegates:
#email: https://example.com # Delegate email sending to example.org #email: https://example.com # Delegate email sending to example.org
#msisdn: http://localhost:8090 # Delegate SMS sending to this local process #msisdn: http://localhost:8090 # Delegate SMS sending to this local process
@ -1107,12 +1112,13 @@ signing_key_path: "CONFDIR/SERVERNAME.signing.key"
# Enable SAML2 for registration and login. Uses pysaml2. # Enable SAML2 for registration and login. Uses pysaml2.
# #
# `sp_config` is the configuration for the pysaml2 Service Provider. # At least one of `sp_config` or `config_path` must be set in this section to
# See pysaml2 docs for format of config. # enable SAML login.
# #
# Default values will be used for the 'entityid' and 'service' settings, # (You will probably also want to set the following options to `false` to
# so it is not normally necessary to specify them unless you need to # disable the regular login/registration flows:
# override them. # * enable_registration
# * password_config.enabled
# #
# Once SAML support is enabled, a metadata file will be exposed at # Once SAML support is enabled, a metadata file will be exposed at
# https://<server>:<port>/_matrix/saml2/metadata.xml, which you may be able to # https://<server>:<port>/_matrix/saml2/metadata.xml, which you may be able to
@ -1120,52 +1126,85 @@ signing_key_path: "CONFDIR/SERVERNAME.signing.key"
# the IdP to use an ACS location of # the IdP to use an ACS location of
# https://<server>:<port>/_matrix/saml2/authn_response. # https://<server>:<port>/_matrix/saml2/authn_response.
# #
#saml2_config: saml2_config:
# sp_config: # `sp_config` is the configuration for the pysaml2 Service Provider.
# # point this to the IdP's metadata. You can use either a local file or # See pysaml2 docs for format of config.
# # (preferably) a URL. #
# metadata: # Default values will be used for the 'entityid' and 'service' settings,
# #local: ["saml2/idp.xml"] # so it is not normally necessary to specify them unless you need to
# remote: # override them.
# - url: https://our_idp/metadata.xml #
# #sp_config:
# # By default, the user has to go to our login page first. If you'd like to # # point this to the IdP's metadata. You can use either a local file or
# # allow IdP-initiated login, set 'allow_unsolicited: True' in a # # (preferably) a URL.
# # 'service.sp' section: # metadata:
# # # #local: ["saml2/idp.xml"]
# #service: # remote:
# # sp: # - url: https://our_idp/metadata.xml
# # allow_unsolicited: True #
# # # By default, the user has to go to our login page first. If you'd like
# # The examples below are just used to generate our metadata xml, and you # # to allow IdP-initiated login, set 'allow_unsolicited: True' in a
# # may well not need it, depending on your setup. Alternatively you # # 'service.sp' section:
# # may need a whole lot more detail - see the pysaml2 docs! # #
# # #service:
# description: ["My awesome SP", "en"] # # sp:
# name: ["Test SP", "en"] # # allow_unsolicited: true
# #
# organization: # # The examples below are just used to generate our metadata xml, and you
# name: Example com # # may well not need them, depending on your setup. Alternatively you
# display_name: # # may need a whole lot more detail - see the pysaml2 docs!
# - ["Example co", "en"] #
# url: "http://example.com" # description: ["My awesome SP", "en"]
# # name: ["Test SP", "en"]
# contact_person: #
# - given_name: Bob # organization:
# sur_name: "the Sysadmin" # name: Example com
# email_address": ["admin@example.com"] # display_name:
# contact_type": technical # - ["Example co", "en"]
# # url: "http://example.com"
# # Instead of putting the config inline as above, you can specify a #
# # separate pysaml2 configuration file: # contact_person:
# # # - given_name: Bob
# config_path: "CONFDIR/sp_conf.py" # sur_name: "the Sysadmin"
# # email_address": ["admin@example.com"]
# # the lifetime of a SAML session. This defines how long a user has to # contact_type": technical
# # complete the authentication process, if allow_unsolicited is unset.
# # The default is 5 minutes. # Instead of putting the config inline as above, you can specify a
# # # separate pysaml2 configuration file:
# # saml_session_lifetime: 5m #
#config_path: "CONFDIR/sp_conf.py"
# the lifetime of a SAML session. This defines how long a user has to
# complete the authentication process, if allow_unsolicited is unset.
# The default is 5 minutes.
#
#saml_session_lifetime: 5m
# The SAML attribute (after mapping via the attribute maps) to use to derive
# the Matrix ID from. 'uid' by default.
#
#mxid_source_attribute: displayName
# The mapping system to use for mapping the saml attribute onto a matrix ID.
# Options include:
# * 'hexencode' (which maps unpermitted characters to '=xx')
# * 'dotreplace' (which replaces unpermitted characters with '.').
# The default is 'hexencode'.
#
#mxid_mapping: dotreplace
# In previous versions of synapse, the mapping from SAML attribute to MXID was
# always calculated dynamically rather than stored in a table. For backwards-
# compatibility, we will look for user_ids matching such a pattern before
# creating a new account.
#
# This setting controls the SAML attribute which will be used for this
# backwards-compatibility lookup. Typically it should be 'uid', but if the
# attribute maps are changed, it may be necessary to change it.
#
# The default is 'uid'.
#
#grandfathered_mxid_source_attribute: upn

View File

@ -293,6 +293,8 @@ class RegistrationConfig(Config):
# by the Matrix Identity Service API specification: # by the Matrix Identity Service API specification:
# https://matrix.org/docs/spec/identity_service/latest # https://matrix.org/docs/spec/identity_service/latest
# #
# If a delegate is specified, the config option public_baseurl must also be filled out.
#
account_threepid_delegates: account_threepid_delegates:
#email: https://example.com # Delegate email sending to example.org #email: https://example.com # Delegate email sending to example.org
#msisdn: http://localhost:8090 # Delegate SMS sending to this local process #msisdn: http://localhost:8090 # Delegate SMS sending to this local process

View File

@ -1,5 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright 2018 New Vector Ltd # Copyright 2018 New Vector Ltd
# Copyright 2019 The Matrix.org Foundation C.I.C.
# #
# Licensed under the Apache License, Version 2.0 (the "License"); # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License. # you may not use this file except in compliance with the License.
@ -12,11 +13,47 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
import re
from synapse.python_dependencies import DependencyException, check_requirements from synapse.python_dependencies import DependencyException, check_requirements
from synapse.types import (
map_username_to_mxid_localpart,
mxid_localpart_allowed_characters,
)
from synapse.util.module_loader import load_python_module
from ._base import Config, ConfigError from ._base import Config, ConfigError
def _dict_merge(merge_dict, into_dict):
"""Do a deep merge of two dicts
Recursively merges `merge_dict` into `into_dict`:
* For keys where both `merge_dict` and `into_dict` have a dict value, the values
are recursively merged
* For all other keys, the values in `into_dict` (if any) are overwritten with
the value from `merge_dict`.
Args:
merge_dict (dict): dict to merge
into_dict (dict): target dict
"""
for k, v in merge_dict.items():
if k not in into_dict:
into_dict[k] = v
continue
current_val = into_dict[k]
if isinstance(v, dict) and isinstance(current_val, dict):
_dict_merge(v, current_val)
continue
# otherwise we just overwrite
into_dict[k] = v
class SAML2Config(Config): class SAML2Config(Config):
def read_config(self, config, **kwargs): def read_config(self, config, **kwargs):
self.saml2_enabled = False self.saml2_enabled = False
@ -26,6 +63,9 @@ class SAML2Config(Config):
if not saml2_config or not saml2_config.get("enabled", True): if not saml2_config or not saml2_config.get("enabled", True):
return return
if not saml2_config.get("sp_config") and not saml2_config.get("config_path"):
return
try: try:
check_requirements("saml2") check_requirements("saml2")
except DependencyException as e: except DependencyException as e:
@ -33,21 +73,40 @@ class SAML2Config(Config):
self.saml2_enabled = True self.saml2_enabled = True
import saml2.config self.saml2_mxid_source_attribute = saml2_config.get(
"mxid_source_attribute", "uid"
)
self.saml2_sp_config = saml2.config.SPConfig() self.saml2_grandfathered_mxid_source_attribute = saml2_config.get(
self.saml2_sp_config.load(self._default_saml_config_dict()) "grandfathered_mxid_source_attribute", "uid"
self.saml2_sp_config.load(saml2_config.get("sp_config", {})) )
saml2_config_dict = self._default_saml_config_dict()
_dict_merge(
merge_dict=saml2_config.get("sp_config", {}), into_dict=saml2_config_dict
)
config_path = saml2_config.get("config_path", None) config_path = saml2_config.get("config_path", None)
if config_path is not None: if config_path is not None:
self.saml2_sp_config.load_file(config_path) mod = load_python_module(config_path)
_dict_merge(merge_dict=mod.CONFIG, into_dict=saml2_config_dict)
import saml2.config
self.saml2_sp_config = saml2.config.SPConfig()
self.saml2_sp_config.load(saml2_config_dict)
# session lifetime: in milliseconds # session lifetime: in milliseconds
self.saml2_session_lifetime = self.parse_duration( self.saml2_session_lifetime = self.parse_duration(
saml2_config.get("saml_session_lifetime", "5m") saml2_config.get("saml_session_lifetime", "5m")
) )
mapping = saml2_config.get("mxid_mapping", "hexencode")
try:
self.saml2_mxid_mapper = MXID_MAPPER_MAP[mapping]
except KeyError:
raise ConfigError("%s is not a known mxid_mapping" % (mapping,))
def _default_saml_config_dict(self): def _default_saml_config_dict(self):
import saml2 import saml2
@ -55,6 +114,13 @@ class SAML2Config(Config):
if public_baseurl is None: if public_baseurl is None:
raise ConfigError("saml2_config requires a public_baseurl to be set") raise ConfigError("saml2_config requires a public_baseurl to be set")
required_attributes = {"uid", self.saml2_mxid_source_attribute}
optional_attributes = {"displayName"}
if self.saml2_grandfathered_mxid_source_attribute:
optional_attributes.add(self.saml2_grandfathered_mxid_source_attribute)
optional_attributes -= required_attributes
metadata_url = public_baseurl + "_matrix/saml2/metadata.xml" metadata_url = public_baseurl + "_matrix/saml2/metadata.xml"
response_url = public_baseurl + "_matrix/saml2/authn_response" response_url = public_baseurl + "_matrix/saml2/authn_response"
return { return {
@ -66,8 +132,9 @@ class SAML2Config(Config):
(response_url, saml2.BINDING_HTTP_POST) (response_url, saml2.BINDING_HTTP_POST)
] ]
}, },
"required_attributes": ["uid"], "required_attributes": list(required_attributes),
"optional_attributes": ["mail", "surname", "givenname"], "optional_attributes": list(optional_attributes),
# "name_id_format": saml2.saml.NAMEID_FORMAT_PERSISTENT,
} }
}, },
} }
@ -76,12 +143,13 @@ class SAML2Config(Config):
return """\ return """\
# Enable SAML2 for registration and login. Uses pysaml2. # Enable SAML2 for registration and login. Uses pysaml2.
# #
# `sp_config` is the configuration for the pysaml2 Service Provider. # At least one of `sp_config` or `config_path` must be set in this section to
# See pysaml2 docs for format of config. # enable SAML login.
# #
# Default values will be used for the 'entityid' and 'service' settings, # (You will probably also want to set the following options to `false` to
# so it is not normally necessary to specify them unless you need to # disable the regular login/registration flows:
# override them. # * enable_registration
# * password_config.enabled
# #
# Once SAML support is enabled, a metadata file will be exposed at # Once SAML support is enabled, a metadata file will be exposed at
# https://<server>:<port>/_matrix/saml2/metadata.xml, which you may be able to # https://<server>:<port>/_matrix/saml2/metadata.xml, which you may be able to
@ -89,52 +157,105 @@ class SAML2Config(Config):
# the IdP to use an ACS location of # the IdP to use an ACS location of
# https://<server>:<port>/_matrix/saml2/authn_response. # https://<server>:<port>/_matrix/saml2/authn_response.
# #
#saml2_config: saml2_config:
# sp_config: # `sp_config` is the configuration for the pysaml2 Service Provider.
# # point this to the IdP's metadata. You can use either a local file or # See pysaml2 docs for format of config.
# # (preferably) a URL. #
# metadata: # Default values will be used for the 'entityid' and 'service' settings,
# #local: ["saml2/idp.xml"] # so it is not normally necessary to specify them unless you need to
# remote: # override them.
# - url: https://our_idp/metadata.xml #
# #sp_config:
# # By default, the user has to go to our login page first. If you'd like to # # point this to the IdP's metadata. You can use either a local file or
# # allow IdP-initiated login, set 'allow_unsolicited: True' in a # # (preferably) a URL.
# # 'service.sp' section: # metadata:
# # # #local: ["saml2/idp.xml"]
# #service: # remote:
# # sp: # - url: https://our_idp/metadata.xml
# # allow_unsolicited: True #
# # # By default, the user has to go to our login page first. If you'd like
# # The examples below are just used to generate our metadata xml, and you # # to allow IdP-initiated login, set 'allow_unsolicited: True' in a
# # may well not need it, depending on your setup. Alternatively you # # 'service.sp' section:
# # may need a whole lot more detail - see the pysaml2 docs! # #
# # #service:
# description: ["My awesome SP", "en"] # # sp:
# name: ["Test SP", "en"] # # allow_unsolicited: true
# #
# organization: # # The examples below are just used to generate our metadata xml, and you
# name: Example com # # may well not need them, depending on your setup. Alternatively you
# display_name: # # may need a whole lot more detail - see the pysaml2 docs!
# - ["Example co", "en"] #
# url: "http://example.com" # description: ["My awesome SP", "en"]
# # name: ["Test SP", "en"]
# contact_person: #
# - given_name: Bob # organization:
# sur_name: "the Sysadmin" # name: Example com
# email_address": ["admin@example.com"] # display_name:
# contact_type": technical # - ["Example co", "en"]
# # url: "http://example.com"
# # Instead of putting the config inline as above, you can specify a #
# # separate pysaml2 configuration file: # contact_person:
# # # - given_name: Bob
# config_path: "%(config_dir_path)s/sp_conf.py" # sur_name: "the Sysadmin"
# # email_address": ["admin@example.com"]
# # the lifetime of a SAML session. This defines how long a user has to # contact_type": technical
# # complete the authentication process, if allow_unsolicited is unset.
# # The default is 5 minutes. # Instead of putting the config inline as above, you can specify a
# # # separate pysaml2 configuration file:
# # saml_session_lifetime: 5m #
#config_path: "%(config_dir_path)s/sp_conf.py"
# the lifetime of a SAML session. This defines how long a user has to
# complete the authentication process, if allow_unsolicited is unset.
# The default is 5 minutes.
#
#saml_session_lifetime: 5m
# The SAML attribute (after mapping via the attribute maps) to use to derive
# the Matrix ID from. 'uid' by default.
#
#mxid_source_attribute: displayName
# The mapping system to use for mapping the saml attribute onto a matrix ID.
# Options include:
# * 'hexencode' (which maps unpermitted characters to '=xx')
# * 'dotreplace' (which replaces unpermitted characters with '.').
# The default is 'hexencode'.
#
#mxid_mapping: dotreplace
# In previous versions of synapse, the mapping from SAML attribute to MXID was
# always calculated dynamically rather than stored in a table. For backwards-
# compatibility, we will look for user_ids matching such a pattern before
# creating a new account.
#
# This setting controls the SAML attribute which will be used for this
# backwards-compatibility lookup. Typically it should be 'uid', but if the
# attribute maps are changed, it may be necessary to change it.
#
# The default is 'uid'.
#
#grandfathered_mxid_source_attribute: upn
""" % { """ % {
"config_dir_path": config_dir_path "config_dir_path": config_dir_path
} }
DOT_REPLACE_PATTERN = re.compile(
("[^%s]" % (re.escape("".join(mxid_localpart_allowed_characters)),))
)
def dot_replace_for_mxid(username: str) -> str:
username = username.lower()
username = DOT_REPLACE_PATTERN.sub(".", username)
# regular mxids aren't allowed to start with an underscore either
username = re.sub("^_", "", username)
return username
MXID_MAPPER_MAP = {
"hexencode": map_username_to_mxid_localpart,
"dotreplace": dot_replace_for_mxid,
}

View File

@ -362,10 +362,8 @@ class ServerConfig(Config):
_check_resource_config(self.listeners) _check_resource_config(self.listeners)
# An experimental option to try and periodically clean up extremities
# by sending dummy events.
self.cleanup_extremities_with_dummy_events = config.get( self.cleanup_extremities_with_dummy_events = config.get(
"cleanup_extremities_with_dummy_events", False "cleanup_extremities_with_dummy_events", True
) )
def has_tls_listener(self): def has_tls_listener(self):
@ -552,6 +550,9 @@ class ServerConfig(Config):
# blacklist IP address CIDR ranges. If this option is not specified, or # blacklist IP address CIDR ranges. If this option is not specified, or
# specified with an empty list, no ip range blacklist will be enforced. # specified with an empty list, no ip range blacklist will be enforced.
# #
# As of Synapse v1.4.0 this option also affects any outbound requests to identity
# servers provided by user input.
#
# (0.0.0.0 and :: are always blacklisted, whether or not they are explicitly # (0.0.0.0 and :: are always blacklisted, whether or not they are explicitly
# listed here, since they correspond to unroutable addresses.) # listed here, since they correspond to unroutable addresses.)
# #

View File

@ -21,10 +21,8 @@ import unicodedata
import attr import attr
import bcrypt import bcrypt
import pymacaroons import pymacaroons
from canonicaljson import json
from twisted.internet import defer from twisted.internet import defer
from twisted.web.client import PartialDownloadError
import synapse.util.stringutils as stringutils import synapse.util.stringutils as stringutils
from synapse.api.constants import LoginType from synapse.api.constants import LoginType
@ -38,7 +36,8 @@ 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.handlers.ui_auth import INTERACTIVE_AUTH_CHECKERS
from synapse.handlers.ui_auth.checkers import UserInteractiveAuthChecker
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
@ -58,13 +57,13 @@ class AuthHandler(BaseHandler):
hs (synapse.server.HomeServer): hs (synapse.server.HomeServer):
""" """
super(AuthHandler, self).__init__(hs) super(AuthHandler, self).__init__(hs)
self.checkers = {
LoginType.RECAPTCHA: self._check_recaptcha, self.checkers = {} # type: dict[str, UserInteractiveAuthChecker]
LoginType.EMAIL_IDENTITY: self._check_email_identity, for auth_checker_class in INTERACTIVE_AUTH_CHECKERS:
LoginType.MSISDN: self._check_msisdn, inst = auth_checker_class(hs)
LoginType.DUMMY: self._check_dummy_auth, if inst.is_enabled():
LoginType.TERMS: self._check_terms_auth, self.checkers[inst.AUTH_TYPE] = inst
}
self.bcrypt_rounds = hs.config.bcrypt_rounds self.bcrypt_rounds = hs.config.bcrypt_rounds
# This is not a cache per se, but a store of all current sessions that # This is not a cache per se, but a store of all current sessions that
@ -158,6 +157,14 @@ class AuthHandler(BaseHandler):
return params return params
def get_enabled_auth_types(self):
"""Return the enabled user-interactive authentication types
Returns the UI-Auth types which are supported by the homeserver's current
config.
"""
return self.checkers.keys()
@defer.inlineCallbacks @defer.inlineCallbacks
def check_auth(self, flows, clientdict, clientip): def check_auth(self, flows, clientdict, clientip):
""" """
@ -292,7 +299,7 @@ class AuthHandler(BaseHandler):
sess["creds"] = {} sess["creds"] = {}
creds = sess["creds"] creds = sess["creds"]
result = yield self.checkers[stagetype](authdict, clientip) result = yield self.checkers[stagetype].check_auth(authdict, clientip)
if result: if result:
creds[stagetype] = result creds[stagetype] = result
self._save_session(sess) self._save_session(sess)
@ -363,7 +370,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:
res = yield checker(authdict, clientip=clientip) res = yield checker.check_auth(authdict, clientip=clientip)
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
@ -376,125 +383,6 @@ class AuthHandler(BaseHandler):
(canonical_id, callback) = yield self.validate_login(user_id, authdict) (canonical_id, callback) = yield self.validate_login(user_id, authdict)
return canonical_id return canonical_id
@defer.inlineCallbacks
def _check_recaptcha(self, authdict, clientip, **kwargs):
try:
user_response = authdict["response"]
except KeyError:
# Client tried to provide captcha but didn't give the parameter:
# bad request.
raise LoginError(
400, "Captcha response is required", errcode=Codes.CAPTCHA_NEEDED
)
logger.info(
"Submitting recaptcha response %s with remoteip %s", user_response, clientip
)
# TODO: get this from the homeserver rather than creating a new one for
# each request
try:
client = self.hs.get_simple_http_client()
resp_body = yield client.post_urlencoded_get_json(
self.hs.config.recaptcha_siteverify_api,
args={
"secret": self.hs.config.recaptcha_private_key,
"response": user_response,
"remoteip": clientip,
},
)
except PartialDownloadError as pde:
# Twisted is silly
data = pde.response
resp_body = json.loads(data)
if "success" in resp_body:
# Note that we do NOT check the hostname here: we explicitly
# intend the CAPTCHA to be presented by whatever client the
# user is using, we just care that they have completed a CAPTCHA.
logger.info(
"%s reCAPTCHA from hostname %s",
"Successful" if resp_body["success"] else "Failed",
resp_body.get("hostname"),
)
if resp_body["success"]:
return True
raise LoginError(401, "", errcode=Codes.UNAUTHORIZED)
def _check_email_identity(self, authdict, **kwargs):
return self._check_threepid("email", authdict, **kwargs)
def _check_msisdn(self, authdict, **kwargs):
return self._check_threepid("msisdn", authdict)
def _check_dummy_auth(self, authdict, **kwargs):
return defer.succeed(True)
def _check_terms_auth(self, authdict, **kwargs):
return defer.succeed(True)
@defer.inlineCallbacks
def _check_threepid(self, medium, authdict, **kwargs):
if "threepid_creds" not in authdict:
raise LoginError(400, "Missing threepid_creds", Codes.MISSING_PARAM)
threepid_creds = authdict["threepid_creds"]
identity_handler = self.hs.get_handlers().identity_handler
logger.info("Getting validated threepid. threepidcreds: %r", (threepid_creds,))
if self.hs.config.threepid_behaviour_email == ThreepidBehaviour.REMOTE:
if medium == "email":
threepid = yield identity_handler.threepid_from_creds(
self.hs.config.account_threepid_delegate_email, threepid_creds
)
elif medium == "msisdn":
threepid = yield identity_handler.threepid_from_creds(
self.hs.config.account_threepid_delegate_msisdn, threepid_creds
)
else:
raise SynapseError(400, "Unrecognized threepid medium: %s" % (medium,))
elif self.hs.config.threepid_behaviour_email == ThreepidBehaviour.LOCAL:
row = yield self.store.get_threepid_validation_session(
medium,
threepid_creds["client_secret"],
sid=threepid_creds["sid"],
validated=True,
)
threepid = (
{
"medium": row["medium"],
"address": row["address"],
"validated_at": row["validated_at"],
}
if row
else None
)
if row:
# Valid threepid returned, delete from the db
yield self.store.delete_threepid_session(threepid_creds["sid"])
else:
raise SynapseError(
400, "Password resets are not enabled on this homeserver"
)
if not threepid:
raise LoginError(401, "", errcode=Codes.UNAUTHORIZED)
if threepid["medium"] != medium:
raise LoginError(
401,
"Expecting threepid of type '%s', got '%s'"
% (medium, threepid["medium"]),
errcode=Codes.UNAUTHORIZED,
)
threepid["threepid_creds"] = authdict["threepid_creds"]
return threepid
def _get_params_recaptcha(self): def _get_params_recaptcha(self):
return {"public_key": self.hs.config.recaptcha_public_key} return {"public_key": self.hs.config.recaptcha_public_key}

View File

@ -73,7 +73,9 @@ class DeactivateAccountHandler(BaseHandler):
# unbinding # unbinding
identity_server_supports_unbinding = True identity_server_supports_unbinding = True
threepids = yield self.store.user_get_threepids(user_id) # Retrieve the 3PIDs this user has bound to an identity server
threepids = yield self.store.user_get_bound_threepids(user_id)
for threepid in threepids: for threepid in threepids:
try: try:
result = yield self._identity_handler.try_unbind_threepid( result = yield self._identity_handler.try_unbind_threepid(

View File

@ -18,6 +18,7 @@
"""Utilities for interacting with Identity Servers""" """Utilities for interacting with Identity Servers"""
import logging import logging
import urllib
from canonicaljson import json from canonicaljson import json
@ -30,6 +31,8 @@ from synapse.api.errors import (
HttpResponseException, HttpResponseException,
SynapseError, SynapseError,
) )
from synapse.config.emailconfig import ThreepidBehaviour
from synapse.http.client import SimpleHttpClient
from synapse.util.stringutils import random_string from synapse.util.stringutils import random_string
from ._base import BaseHandler from ._base import BaseHandler
@ -41,40 +44,15 @@ class IdentityHandler(BaseHandler):
def __init__(self, hs): def __init__(self, hs):
super(IdentityHandler, self).__init__(hs) super(IdentityHandler, self).__init__(hs)
self.http_client = hs.get_simple_http_client() self.http_client = SimpleHttpClient(hs)
# We create a blacklisting instance of SimpleHttpClient for contacting identity
# servers specified by clients
self.blacklisting_http_client = SimpleHttpClient(
hs, ip_blacklist=hs.config.federation_ip_range_blacklist
)
self.federation_http_client = hs.get_http_client() self.federation_http_client = hs.get_http_client()
self.hs = hs self.hs = hs
def _extract_items_from_creds_dict(self, creds):
"""
Retrieve entries from a "credentials" dictionary
Args:
creds (dict[str, str]): Dictionary of credentials that contain the following keys:
* client_secret|clientSecret: A unique secret str provided by the client
* id_server|idServer: the domain of the identity server to query
* id_access_token: The access token to authenticate to the identity
server with.
Returns:
tuple(str, str, str|None): A tuple containing the client_secret, the id_server,
and the id_access_token value if available.
"""
client_secret = creds.get("client_secret") or creds.get("clientSecret")
if not client_secret:
raise SynapseError(
400, "No client_secret in creds", errcode=Codes.MISSING_PARAM
)
id_server = creds.get("id_server") or creds.get("idServer")
if not id_server:
raise SynapseError(
400, "No id_server in creds", errcode=Codes.MISSING_PARAM
)
id_access_token = creds.get("id_access_token")
return client_secret, id_server, id_access_token
@defer.inlineCallbacks @defer.inlineCallbacks
def threepid_from_creds(self, id_server, creds): def threepid_from_creds(self, id_server, creds):
""" """
@ -113,35 +91,50 @@ class IdentityHandler(BaseHandler):
data = yield self.http_client.get_json(url, query_params) data = yield self.http_client.get_json(url, query_params)
except TimeoutError: except TimeoutError:
raise SynapseError(500, "Timed out contacting identity server") raise SynapseError(500, "Timed out contacting identity server")
return data if "medium" in data else None except HttpResponseException as e:
logger.info(
"%s returned %i for threepid validation for: %s",
id_server,
e.code,
creds,
)
return None
# Old versions of Sydent return a 200 http code even on a failed validation
# check. Thus, in addition to the HttpResponseException check above (which
# checks for non-200 errors), we need to make sure validation_session isn't
# actually an error, identified by the absence of a "medium" key
# See https://github.com/matrix-org/sydent/issues/215 for details
if "medium" in data:
return data
logger.info("%s reported non-validated threepid: %s", id_server, creds)
return None
@defer.inlineCallbacks @defer.inlineCallbacks
def bind_threepid(self, creds, mxid, use_v2=True): def bind_threepid(
self, client_secret, sid, mxid, id_server, id_access_token=None, use_v2=True
):
"""Bind a 3PID to an identity server """Bind a 3PID to an identity server
Args: Args:
creds (dict[str, str]): Dictionary of credentials that contain the following keys: client_secret (str): A unique secret provided by the client
* client_secret|clientSecret: A unique secret str provided by the client
* id_server|idServer: the domain of the identity server to query sid (str): The ID of the validation session
* id_access_token: The access token to authenticate to the identity
server with. Required if use_v2 is true
mxid (str): The MXID to bind the 3PID to mxid (str): The MXID to bind the 3PID to
use_v2 (bool): Whether to use v2 Identity Service API endpoints
id_server (str): The domain of the identity server to query
id_access_token (str): The access token to authenticate to the identity
server with, if necessary. Required if use_v2 is true
use_v2 (bool): Whether to use v2 Identity Service API endpoints. Defaults to True
Returns: Returns:
Deferred[dict]: The response from the identity server Deferred[dict]: The response from the identity server
""" """
logger.debug("binding threepid %r to %s", creds, mxid) logger.debug("Proxying threepid bind request for %s to %s", mxid, id_server)
client_secret, id_server, id_access_token = self._extract_items_from_creds_dict(
creds
)
sid = creds.get("sid")
if not sid:
raise SynapseError(
400, "No sid in three_pid_creds", errcode=Codes.MISSING_PARAM
)
# If an id_access_token is not supplied, force usage of v1 # If an id_access_token is not supplied, force usage of v1
if id_access_token is None: if id_access_token is None:
@ -157,10 +150,11 @@ class IdentityHandler(BaseHandler):
bind_url = "https://%s/_matrix/identity/api/v1/3pid/bind" % (id_server,) bind_url = "https://%s/_matrix/identity/api/v1/3pid/bind" % (id_server,)
try: try:
data = yield self.http_client.post_json_get_json( # Use the blacklisting http client as this call is only to identity servers
# provided by a client
data = yield self.blacklisting_http_client.post_json_get_json(
bind_url, bind_data, headers=headers bind_url, bind_data, headers=headers
) )
logger.debug("bound threepid %r to %s", creds, mxid)
# Remember where we bound the threepid # Remember where we bound the threepid
yield self.store.add_user_bound_threepid( yield self.store.add_user_bound_threepid(
@ -182,7 +176,10 @@ class IdentityHandler(BaseHandler):
return data return data
logger.info("Got 404 when POSTing JSON %s, falling back to v1 URL", bind_url) logger.info("Got 404 when POSTing JSON %s, falling back to v1 URL", bind_url)
return (yield self.bind_threepid(creds, mxid, use_v2=False)) res = yield self.bind_threepid(
client_secret, sid, mxid, id_server, id_access_token, use_v2=False
)
return res
@defer.inlineCallbacks @defer.inlineCallbacks
def try_unbind_threepid(self, mxid, threepid): def try_unbind_threepid(self, mxid, threepid):
@ -258,7 +255,11 @@ class IdentityHandler(BaseHandler):
headers = {b"Authorization": auth_headers} headers = {b"Authorization": auth_headers}
try: try:
yield self.http_client.post_json_get_json(url, content, headers) # Use the blacklisting http client as this call is only to identity servers
# provided by a client
yield self.blacklisting_http_client.post_json_get_json(
url, content, headers
)
changed = True changed = True
except HttpResponseException as e: except HttpResponseException as e:
changed = False changed = False
@ -328,6 +329,15 @@ class IdentityHandler(BaseHandler):
# Generate a session id # Generate a session id
session_id = random_string(16) session_id = random_string(16)
if next_link:
# Manipulate the next_link to add the sid, because the caller won't get
# it until we send a response, by which time we've sent the mail.
if "?" in next_link:
next_link += "&"
else:
next_link += "?"
next_link += "sid=" + urllib.parse.quote(session_id)
# Generate a new validation token # Generate a new validation token
token = random_string(32) token = random_string(32)
@ -452,13 +462,101 @@ class IdentityHandler(BaseHandler):
id_server + "/_matrix/identity/api/v1/validate/msisdn/requestToken", id_server + "/_matrix/identity/api/v1/validate/msisdn/requestToken",
params, params,
) )
return data
except HttpResponseException as e: except HttpResponseException as e:
logger.info("Proxied requestToken failed: %r", e) logger.info("Proxied requestToken failed: %r", e)
raise e.to_synapse_error() raise e.to_synapse_error()
except TimeoutError: except TimeoutError:
raise SynapseError(500, "Timed out contacting identity server") raise SynapseError(500, "Timed out contacting identity server")
assert self.hs.config.public_baseurl
# we need to tell the client to send the token back to us, since it doesn't
# otherwise know where to send it, so add submit_url response parameter
# (see also MSC2078)
data["submit_url"] = (
self.hs.config.public_baseurl
+ "_matrix/client/unstable/add_threepid/msisdn/submit_token"
)
return data
@defer.inlineCallbacks
def validate_threepid_session(self, client_secret, sid):
"""Validates a threepid session with only the client secret and session ID
Tries validating against any configured account_threepid_delegates as well as locally.
Args:
client_secret (str): A secret provided by the client
sid (str): The ID of the session
Returns:
Dict[str, str|int] if validation was successful, otherwise None
"""
# XXX: We shouldn't need to keep wrapping and unwrapping this value
threepid_creds = {"client_secret": client_secret, "sid": sid}
# We don't actually know which medium this 3PID is. Thus we first assume it's email,
# and if validation fails we try msisdn
validation_session = None
# Try to validate as email
if self.hs.config.threepid_behaviour_email == ThreepidBehaviour.REMOTE:
# Ask our delegated email identity server
validation_session = yield self.threepid_from_creds(
self.hs.config.account_threepid_delegate_email, threepid_creds
)
elif self.hs.config.threepid_behaviour_email == ThreepidBehaviour.LOCAL:
# Get a validated session matching these details
validation_session = yield self.store.get_threepid_validation_session(
"email", client_secret, sid=sid, validated=True
)
if validation_session:
return validation_session
# Try to validate as msisdn
if self.hs.config.account_threepid_delegate_msisdn:
# Ask our delegated msisdn identity server
validation_session = yield self.threepid_from_creds(
self.hs.config.account_threepid_delegate_msisdn, threepid_creds
)
return validation_session
@defer.inlineCallbacks
def proxy_msisdn_submit_token(self, id_server, client_secret, sid, token):
"""Proxy a POST submitToken request to an identity server for verification purposes
Args:
id_server (str): The identity server URL to contact
client_secret (str): Secret provided by the client
sid (str): The ID of the session
token (str): The verification token
Raises:
SynapseError: If we failed to contact the identity server
Returns:
Deferred[dict]: The response dict from the identity server
"""
body = {"client_secret": client_secret, "sid": sid, "token": token}
try:
return (
yield self.http_client.post_json_get_json(
id_server + "/_matrix/identity/api/v1/validate/msisdn/submitToken",
body,
)
)
except TimeoutError:
raise SynapseError(500, "Timed out contacting identity server")
except HttpResponseException as e:
logger.warning("Error contacting msisdn account_threepid_delegate: %s", e)
raise SynapseError(400, "Error contacting the identity server")
def create_id_access_token_header(id_access_token): def create_id_access_token_header(id_access_token):
"""Create an Authorization header for passing to SimpleHttpClient as the header value """Create an Authorization header for passing to SimpleHttpClient as the header value

View File

@ -31,6 +31,7 @@ from synapse import types
from synapse.api.constants import EventTypes, Membership from synapse.api.constants import EventTypes, Membership
from synapse.api.errors import AuthError, Codes, HttpResponseException, SynapseError from synapse.api.errors import AuthError, Codes, HttpResponseException, SynapseError
from synapse.handlers.identity import LookupAlgorithm, create_id_access_token_header from synapse.handlers.identity import LookupAlgorithm, create_id_access_token_header
from synapse.http.client import SimpleHttpClient
from synapse.types import RoomID, UserID from synapse.types import RoomID, UserID
from synapse.util.async_helpers import Linearizer from synapse.util.async_helpers import Linearizer
from synapse.util.distributor import user_joined_room, user_left_room from synapse.util.distributor import user_joined_room, user_left_room
@ -62,7 +63,11 @@ class RoomMemberHandler(object):
self.auth = hs.get_auth() self.auth = hs.get_auth()
self.state_handler = hs.get_state_handler() self.state_handler = hs.get_state_handler()
self.config = hs.config self.config = hs.config
self.simple_http_client = hs.get_simple_http_client() # We create a blacklisting instance of SimpleHttpClient for contacting identity
# servers specified by clients
self.simple_http_client = SimpleHttpClient(
hs, ip_blacklist=hs.config.federation_ip_range_blacklist
)
self.federation_handler = hs.get_handlers().federation_handler self.federation_handler = hs.get_handlers().federation_handler
self.directory_handler = hs.get_handlers().directory_handler self.directory_handler = hs.get_handlers().directory_handler

View File

@ -21,6 +21,8 @@ from saml2.client import Saml2Client
from synapse.api.errors import SynapseError from synapse.api.errors import SynapseError
from synapse.http.servlet import parse_string from synapse.http.servlet import parse_string
from synapse.rest.client.v1.login import SSOAuthHandler from synapse.rest.client.v1.login import SSOAuthHandler
from synapse.types import UserID, map_username_to_mxid_localpart
from synapse.util.async_helpers import Linearizer
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -29,12 +31,26 @@ class SamlHandler:
def __init__(self, hs): def __init__(self, hs):
self._saml_client = Saml2Client(hs.config.saml2_sp_config) self._saml_client = Saml2Client(hs.config.saml2_sp_config)
self._sso_auth_handler = SSOAuthHandler(hs) self._sso_auth_handler = SSOAuthHandler(hs)
self._registration_handler = hs.get_registration_handler()
self._clock = hs.get_clock()
self._datastore = hs.get_datastore()
self._hostname = hs.hostname
self._saml2_session_lifetime = hs.config.saml2_session_lifetime
self._mxid_source_attribute = hs.config.saml2_mxid_source_attribute
self._grandfathered_mxid_source_attribute = (
hs.config.saml2_grandfathered_mxid_source_attribute
)
self._mxid_mapper = hs.config.saml2_mxid_mapper
# identifier for the external_ids table
self._auth_provider_id = "saml"
# a map from saml session id to Saml2SessionData object # a map from saml session id to Saml2SessionData object
self._outstanding_requests_dict = {} self._outstanding_requests_dict = {}
self._clock = hs.get_clock() # a lock on the mappings
self._saml2_session_lifetime = hs.config.saml2_session_lifetime self._mapping_lock = Linearizer(name="saml_mapping", clock=self._clock)
def handle_redirect_request(self, client_redirect_url): def handle_redirect_request(self, client_redirect_url):
"""Handle an incoming request to /login/sso/redirect """Handle an incoming request to /login/sso/redirect
@ -60,7 +76,7 @@ class SamlHandler:
# this shouldn't happen! # this shouldn't happen!
raise Exception("prepare_for_authenticate didn't return a Location header") raise Exception("prepare_for_authenticate didn't return a Location header")
def handle_saml_response(self, request): async def handle_saml_response(self, request):
"""Handle an incoming request to /_matrix/saml2/authn_response """Handle an incoming request to /_matrix/saml2/authn_response
Args: Args:
@ -77,6 +93,10 @@ class SamlHandler:
# the dict. # the dict.
self.expire_sessions() self.expire_sessions()
user_id = await self._map_saml_response_to_user(resp_bytes)
self._sso_auth_handler.complete_sso_login(user_id, request, relay_state)
async def _map_saml_response_to_user(self, resp_bytes):
try: try:
saml2_auth = self._saml_client.parse_authn_request_response( saml2_auth = self._saml_client.parse_authn_request_response(
resp_bytes, resp_bytes,
@ -91,18 +111,88 @@ class SamlHandler:
logger.warning("SAML2 response was not signed") logger.warning("SAML2 response was not signed")
raise SynapseError(400, "SAML2 response was not signed") raise SynapseError(400, "SAML2 response was not signed")
if "uid" not in saml2_auth.ava: logger.info("SAML2 response: %s", saml2_auth.origxml)
logger.info("SAML2 mapped attributes: %s", saml2_auth.ava)
try:
remote_user_id = saml2_auth.ava["uid"][0]
except KeyError:
logger.warning("SAML2 response lacks a 'uid' attestation") logger.warning("SAML2 response lacks a 'uid' attestation")
raise SynapseError(400, "uid not in SAML2 response") raise SynapseError(400, "uid not in SAML2 response")
try:
mxid_source = saml2_auth.ava[self._mxid_source_attribute][0]
except KeyError:
logger.warning(
"SAML2 response lacks a '%s' attestation", self._mxid_source_attribute
)
raise SynapseError(
400, "%s not in SAML2 response" % (self._mxid_source_attribute,)
)
self._outstanding_requests_dict.pop(saml2_auth.in_response_to, None) self._outstanding_requests_dict.pop(saml2_auth.in_response_to, None)
username = saml2_auth.ava["uid"][0]
displayName = saml2_auth.ava.get("displayName", [None])[0] displayName = saml2_auth.ava.get("displayName", [None])[0]
return self._sso_auth_handler.on_successful_auth( with (await self._mapping_lock.queue(self._auth_provider_id)):
username, request, relay_state, user_display_name=displayName # first of all, check if we already have a mapping for this user
) logger.info(
"Looking for existing mapping for user %s:%s",
self._auth_provider_id,
remote_user_id,
)
registered_user_id = await self._datastore.get_user_by_external_id(
self._auth_provider_id, remote_user_id
)
if registered_user_id is not None:
logger.info("Found existing mapping %s", registered_user_id)
return registered_user_id
# backwards-compatibility hack: see if there is an existing user with a
# suitable mapping from the uid
if (
self._grandfathered_mxid_source_attribute
and self._grandfathered_mxid_source_attribute in saml2_auth.ava
):
attrval = saml2_auth.ava[self._grandfathered_mxid_source_attribute][0]
user_id = UserID(
map_username_to_mxid_localpart(attrval), self._hostname
).to_string()
logger.info(
"Looking for existing account based on mapped %s %s",
self._grandfathered_mxid_source_attribute,
user_id,
)
users = await self._datastore.get_users_by_id_case_insensitive(user_id)
if users:
registered_user_id = list(users.keys())[0]
logger.info("Grandfathering mapping to %s", registered_user_id)
await self._datastore.record_user_external_id(
self._auth_provider_id, remote_user_id, registered_user_id
)
return registered_user_id
# figure out a new mxid for this user
base_mxid_localpart = self._mxid_mapper(mxid_source)
suffix = 0
while True:
localpart = base_mxid_localpart + (str(suffix) if suffix else "")
if not await self._datastore.get_users_by_id_case_insensitive(
UserID(localpart, self._hostname).to_string()
):
break
suffix += 1
logger.info("Allocating mxid for new user with localpart %s", localpart)
registered_user_id = await self._registration_handler.register_user(
localpart=localpart, default_display_name=displayName
)
await self._datastore.record_user_external_id(
self._auth_provider_id, remote_user_id, registered_user_id
)
return registered_user_id
def expire_sessions(self): def expire_sessions(self):
expire_before = self._clock.time_msec() - self._saml2_session_lifetime expire_before = self._clock.time_msec() - self._saml2_session_lifetime

View File

@ -0,0 +1,22 @@
# -*- coding: utf-8 -*-
# Copyright 2019 The Matrix.org Foundation C.I.C.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""This module implements user-interactive auth verification.
TODO: move more stuff out of AuthHandler in here.
"""
from synapse.handlers.ui_auth.checkers import INTERACTIVE_AUTH_CHECKERS # noqa: F401

View File

@ -0,0 +1,247 @@
# -*- coding: utf-8 -*-
# Copyright 2019 The Matrix.org Foundation C.I.C.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import logging
from canonicaljson import json
from twisted.internet import defer
from twisted.web.client import PartialDownloadError
from synapse.api.constants import LoginType
from synapse.api.errors import Codes, LoginError, SynapseError
from synapse.config.emailconfig import ThreepidBehaviour
logger = logging.getLogger(__name__)
class UserInteractiveAuthChecker:
"""Abstract base class for an interactive auth checker"""
def __init__(self, hs):
pass
def is_enabled(self):
"""Check if the configuration of the homeserver allows this checker to work
Returns:
bool: True if this login type is enabled.
"""
def check_auth(self, authdict, clientip):
"""Given the authentication dict from the client, attempt to check this step
Args:
authdict (dict): authentication dictionary from the client
clientip (str): The IP address of the client.
Raises:
SynapseError if authentication failed
Returns:
Deferred: the result of authentication (to pass back to the client?)
"""
raise NotImplementedError()
class DummyAuthChecker(UserInteractiveAuthChecker):
AUTH_TYPE = LoginType.DUMMY
def is_enabled(self):
return True
def check_auth(self, authdict, clientip):
return defer.succeed(True)
class TermsAuthChecker(UserInteractiveAuthChecker):
AUTH_TYPE = LoginType.TERMS
def is_enabled(self):
return True
def check_auth(self, authdict, clientip):
return defer.succeed(True)
class RecaptchaAuthChecker(UserInteractiveAuthChecker):
AUTH_TYPE = LoginType.RECAPTCHA
def __init__(self, hs):
super().__init__(hs)
self._enabled = bool(hs.config.recaptcha_private_key)
self._http_client = hs.get_simple_http_client()
self._url = hs.config.recaptcha_siteverify_api
self._secret = hs.config.recaptcha_private_key
def is_enabled(self):
return self._enabled
@defer.inlineCallbacks
def check_auth(self, authdict, clientip):
try:
user_response = authdict["response"]
except KeyError:
# Client tried to provide captcha but didn't give the parameter:
# bad request.
raise LoginError(
400, "Captcha response is required", errcode=Codes.CAPTCHA_NEEDED
)
logger.info(
"Submitting recaptcha response %s with remoteip %s", user_response, clientip
)
# TODO: get this from the homeserver rather than creating a new one for
# each request
try:
resp_body = yield self._http_client.post_urlencoded_get_json(
self._url,
args={
"secret": self._secret,
"response": user_response,
"remoteip": clientip,
},
)
except PartialDownloadError as pde:
# Twisted is silly
data = pde.response
resp_body = json.loads(data)
if "success" in resp_body:
# Note that we do NOT check the hostname here: we explicitly
# intend the CAPTCHA to be presented by whatever client the
# user is using, we just care that they have completed a CAPTCHA.
logger.info(
"%s reCAPTCHA from hostname %s",
"Successful" if resp_body["success"] else "Failed",
resp_body.get("hostname"),
)
if resp_body["success"]:
return True
raise LoginError(401, "", errcode=Codes.UNAUTHORIZED)
class _BaseThreepidAuthChecker:
def __init__(self, hs):
self.hs = hs
self.store = hs.get_datastore()
@defer.inlineCallbacks
def _check_threepid(self, medium, authdict):
if "threepid_creds" not in authdict:
raise LoginError(400, "Missing threepid_creds", Codes.MISSING_PARAM)
threepid_creds = authdict["threepid_creds"]
identity_handler = self.hs.get_handlers().identity_handler
logger.info("Getting validated threepid. threepidcreds: %r", (threepid_creds,))
# msisdns are currently always ThreepidBehaviour.REMOTE
if medium == "msisdn":
if not self.hs.config.account_threepid_delegate_msisdn:
raise SynapseError(
400, "Phone number verification is not enabled on this homeserver"
)
threepid = yield identity_handler.threepid_from_creds(
self.hs.config.account_threepid_delegate_msisdn, threepid_creds
)
elif medium == "email":
if self.hs.config.threepid_behaviour_email == ThreepidBehaviour.REMOTE:
assert self.hs.config.account_threepid_delegate_email
threepid = yield identity_handler.threepid_from_creds(
self.hs.config.account_threepid_delegate_email, threepid_creds
)
elif self.hs.config.threepid_behaviour_email == ThreepidBehaviour.LOCAL:
threepid = None
row = yield self.store.get_threepid_validation_session(
medium,
threepid_creds["client_secret"],
sid=threepid_creds["sid"],
validated=True,
)
if row:
threepid = {
"medium": row["medium"],
"address": row["address"],
"validated_at": row["validated_at"],
}
# Valid threepid returned, delete from the db
yield self.store.delete_threepid_session(threepid_creds["sid"])
else:
raise SynapseError(
400, "Email address verification is not enabled on this homeserver"
)
else:
# this can't happen!
raise AssertionError("Unrecognized threepid medium: %s" % (medium,))
if not threepid:
raise LoginError(401, "", errcode=Codes.UNAUTHORIZED)
if threepid["medium"] != medium:
raise LoginError(
401,
"Expecting threepid of type '%s', got '%s'"
% (medium, threepid["medium"]),
errcode=Codes.UNAUTHORIZED,
)
threepid["threepid_creds"] = authdict["threepid_creds"]
return threepid
class EmailIdentityAuthChecker(UserInteractiveAuthChecker, _BaseThreepidAuthChecker):
AUTH_TYPE = LoginType.EMAIL_IDENTITY
def __init__(self, hs):
UserInteractiveAuthChecker.__init__(self, hs)
_BaseThreepidAuthChecker.__init__(self, hs)
def is_enabled(self):
return self.hs.config.threepid_behaviour_email in (
ThreepidBehaviour.REMOTE,
ThreepidBehaviour.LOCAL,
)
def check_auth(self, authdict, clientip):
return self._check_threepid("email", authdict)
class MsisdnAuthChecker(UserInteractiveAuthChecker, _BaseThreepidAuthChecker):
AUTH_TYPE = LoginType.MSISDN
def __init__(self, hs):
UserInteractiveAuthChecker.__init__(self, hs)
_BaseThreepidAuthChecker.__init__(self, hs)
def is_enabled(self):
return bool(self.hs.config.account_threepid_delegate_msisdn)
def check_auth(self, authdict, clientip):
return self._check_threepid("msisdn", authdict)
INTERACTIVE_AUTH_CHECKERS = [
DummyAuthChecker,
TermsAuthChecker,
RecaptchaAuthChecker,
EmailIdentityAuthChecker,
MsisdnAuthChecker,
]
"""A list of UserInteractiveAuthChecker classes"""

View File

@ -29,6 +29,7 @@ from synapse.http.servlet import (
parse_json_object_from_request, parse_json_object_from_request,
parse_string, parse_string,
) )
from synapse.http.site import SynapseRequest
from synapse.rest.client.v2_alpha._base import client_patterns from synapse.rest.client.v2_alpha._base import client_patterns
from synapse.rest.well_known import WellKnownBuilder from synapse.rest.well_known import WellKnownBuilder
from synapse.types import UserID, map_username_to_mxid_localpart from synapse.types import UserID, map_username_to_mxid_localpart
@ -507,6 +508,19 @@ class SSOAuthHandler(object):
localpart=localpart, default_display_name=user_display_name localpart=localpart, default_display_name=user_display_name
) )
self.complete_sso_login(registered_user_id, request, client_redirect_url)
def complete_sso_login(
self, registered_user_id: str, request: SynapseRequest, client_redirect_url: str
):
"""Having figured out a mxid for this user, complete the HTTP request
Args:
registered_user_id:
request:
client_redirect_url:
"""
login_token = self._macaroon_gen.generate_short_term_login_token( login_token = self._macaroon_gen.generate_short_term_login_token(
registered_user_id registered_user_id
) )

View File

@ -21,12 +21,7 @@ from six.moves import http_client
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 ( from synapse.api.errors import Codes, SynapseError, ThreepidValidationError
Codes,
HttpResponseException,
SynapseError,
ThreepidValidationError,
)
from synapse.config.emailconfig import ThreepidBehaviour 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 (
@ -485,10 +480,8 @@ class MsisdnThreepidRequestTokenRestServlet(RestServlet):
def on_POST(self, request): def on_POST(self, request):
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"],
) )
id_server = "https://" + body["id_server"] # Assume https
client_secret = body["client_secret"] client_secret = body["client_secret"]
country = body["country"] country = body["country"]
phone_number = body["phone_number"] phone_number = body["phone_number"]
@ -509,14 +502,29 @@ class MsisdnThreepidRequestTokenRestServlet(RestServlet):
if existing_user_id 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)
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,
"Adding phone numbers to user account is not supported by this homeserver",
)
ret = yield self.identity_handler.requestMsisdnToken( ret = yield self.identity_handler.requestMsisdnToken(
id_server, country, phone_number, client_secret, send_attempt, next_link self.hs.config.account_threepid_delegate_msisdn,
country,
phone_number,
client_secret,
send_attempt,
next_link,
) )
return 200, ret return 200, ret
class AddThreepidSubmitTokenServlet(RestServlet): class AddThreepidEmailSubmitTokenServlet(RestServlet):
"""Handles 3PID validation token submission for adding an email to a user's account""" """Handles 3PID validation token submission for adding an email to a user's account"""
PATTERNS = client_patterns( PATTERNS = client_patterns(
@ -592,6 +600,48 @@ class AddThreepidSubmitTokenServlet(RestServlet):
finish_request(request) finish_request(request)
class AddThreepidMsisdnSubmitTokenServlet(RestServlet):
"""Handles 3PID validation token submission for adding a phone number to a user's
account
"""
PATTERNS = client_patterns(
"/add_threepid/msisdn/submit_token$", releases=(), unstable=True
)
def __init__(self, hs):
"""
Args:
hs (synapse.server.HomeServer): server
"""
super().__init__()
self.config = hs.config
self.clock = hs.get_clock()
self.store = hs.get_datastore()
self.identity_handler = hs.get_handlers().identity_handler
@defer.inlineCallbacks
def on_POST(self, request):
if not self.config.account_threepid_delegate_msisdn:
raise SynapseError(
400,
"This homeserver is not validating phone numbers. Use an identity server "
"instead.",
)
body = parse_json_object_from_request(request)
assert_params_in_dict(body, ["client_secret", "sid", "token"])
# Proxy submit_token request to msisdn threepid delegate
response = yield self.identity_handler.proxy_msisdn_submit_token(
self.config.account_threepid_delegate_msisdn,
body["client_secret"],
body["sid"],
body["token"],
)
return 200, response
class ThreepidRestServlet(RestServlet): class ThreepidRestServlet(RestServlet):
PATTERNS = client_patterns("/account/3pid$") PATTERNS = client_patterns("/account/3pid$")
@ -627,80 +677,87 @@ class ThreepidRestServlet(RestServlet):
client_secret = threepid_creds["client_secret"] client_secret = threepid_creds["client_secret"]
sid = threepid_creds["sid"] sid = threepid_creds["sid"]
# We don't actually know which medium this 3PID is. Thus we first assume it's email, validation_session = yield self.identity_handler.validate_threepid_session(
# and if validation fails we try msisdn client_secret, sid
validation_session = None )
if validation_session:
# Try to validate as email yield self.auth_handler.add_threepid(
if self.hs.config.threepid_behaviour_email == ThreepidBehaviour.REMOTE: user_id,
# Ask our delegated email identity server validation_session["medium"],
try: validation_session["address"],
validation_session = yield self.identity_handler.threepid_from_creds( validation_session["validated_at"],
self.hs.config.account_threepid_delegate_email, threepid_creds
)
except HttpResponseException:
logger.debug(
"%s reported non-validated threepid: %s",
self.hs.config.account_threepid_delegate_email,
threepid_creds,
)
elif self.hs.config.threepid_behaviour_email == ThreepidBehaviour.LOCAL:
# Get a validated session matching these details
validation_session = yield self.datastore.get_threepid_validation_session(
"email", client_secret, sid=sid, validated=True
) )
# Old versions of Sydent return a 200 http code even on a failed validation check.
# Thus, in addition to the HttpResponseException check above (which checks for
# non-200 errors), we need to make sure validation_session isn't actually an error,
# identified by containing an "error" key
# See https://github.com/matrix-org/sydent/issues/215 for details
if validation_session and "error" not in validation_session:
yield self._add_threepid_to_account(user_id, validation_session)
return 200, {} return 200, {}
# Try to validate as msisdn
if self.hs.config.account_threepid_delegate_msisdn:
# Ask our delegated msisdn identity server
try:
validation_session = yield self.identity_handler.threepid_from_creds(
self.hs.config.account_threepid_delegate_msisdn, threepid_creds
)
except HttpResponseException:
logger.debug(
"%s reported non-validated threepid: %s",
self.hs.config.account_threepid_delegate_email,
threepid_creds,
)
# Check that validation_session isn't actually an error due to old Sydent instances
# See explanatory comment above
if validation_session and "error" not in validation_session:
yield self._add_threepid_to_account(user_id, validation_session)
return 200, {}
raise SynapseError( raise SynapseError(
400, "No validated 3pid session found", Codes.THREEPID_AUTH_FAILED 400, "No validated 3pid session found", Codes.THREEPID_AUTH_FAILED
) )
class ThreepidAddRestServlet(RestServlet):
PATTERNS = client_patterns("/account/3pid/add$", releases=(), unstable=True)
def __init__(self, hs):
super(ThreepidAddRestServlet, self).__init__()
self.hs = hs
self.identity_handler = hs.get_handlers().identity_handler
self.auth = hs.get_auth()
self.auth_handler = hs.get_auth_handler()
@defer.inlineCallbacks @defer.inlineCallbacks
def _add_threepid_to_account(self, user_id, validation_session): def on_POST(self, request):
"""Add a threepid wrapped in a validation_session dict to an account requester = yield self.auth.get_user_by_req(request)
user_id = requester.user.to_string()
body = parse_json_object_from_request(request)
Args: assert_params_in_dict(body, ["client_secret", "sid"])
user_id (str): The mxid of the user to add this 3PID to client_secret = body["client_secret"]
sid = body["sid"]
validation_session (dict): A dict containing the following: validation_session = yield self.identity_handler.validate_threepid_session(
* medium - medium of the threepid client_secret, sid
* address - address of the threepid
* validated_at - timestamp of when the validation occurred
"""
yield self.auth_handler.add_threepid(
user_id,
validation_session["medium"],
validation_session["address"],
validation_session["validated_at"],
) )
if validation_session:
yield self.auth_handler.add_threepid(
user_id,
validation_session["medium"],
validation_session["address"],
validation_session["validated_at"],
)
return 200, {}
raise SynapseError(
400, "No validated 3pid session found", Codes.THREEPID_AUTH_FAILED
)
class ThreepidBindRestServlet(RestServlet):
PATTERNS = client_patterns("/account/3pid/bind$", releases=(), unstable=True)
def __init__(self, hs):
super(ThreepidBindRestServlet, self).__init__()
self.hs = hs
self.identity_handler = hs.get_handlers().identity_handler
self.auth = hs.get_auth()
@defer.inlineCallbacks
def on_POST(self, request):
body = parse_json_object_from_request(request)
assert_params_in_dict(body, ["id_server", "sid", "client_secret"])
id_server = body["id_server"]
sid = body["sid"]
client_secret = body["client_secret"]
id_access_token = body.get("id_access_token") # optional
requester = yield self.auth.get_user_by_req(request)
user_id = requester.user.to_string()
yield self.identity_handler.bind_threepid(
client_secret, sid, user_id, id_server, id_access_token
)
return 200, {}
class ThreepidUnbindRestServlet(RestServlet): class ThreepidUnbindRestServlet(RestServlet):
@ -792,8 +849,11 @@ def register_servlets(hs, http_server):
DeactivateAccountRestServlet(hs).register(http_server) DeactivateAccountRestServlet(hs).register(http_server)
EmailThreepidRequestTokenRestServlet(hs).register(http_server) EmailThreepidRequestTokenRestServlet(hs).register(http_server)
MsisdnThreepidRequestTokenRestServlet(hs).register(http_server) MsisdnThreepidRequestTokenRestServlet(hs).register(http_server)
AddThreepidSubmitTokenServlet(hs).register(http_server) AddThreepidEmailSubmitTokenServlet(hs).register(http_server)
AddThreepidMsisdnSubmitTokenServlet(hs).register(http_server)
ThreepidRestServlet(hs).register(http_server) ThreepidRestServlet(hs).register(http_server)
ThreepidAddRestServlet(hs).register(http_server)
ThreepidBindRestServlet(hs).register(http_server)
ThreepidUnbindRestServlet(hs).register(http_server) ThreepidUnbindRestServlet(hs).register(http_server)
ThreepidDeleteRestServlet(hs).register(http_server) ThreepidDeleteRestServlet(hs).register(http_server)
WhoamiRestServlet(hs).register(http_server) WhoamiRestServlet(hs).register(http_server)

View File

@ -16,6 +16,7 @@
import hmac import hmac
import logging import logging
from typing import List, Union
from six import string_types from six import string_types
@ -31,9 +32,14 @@ from synapse.api.errors import (
ThreepidValidationError, ThreepidValidationError,
UnrecognizedRequestError, UnrecognizedRequestError,
) )
from synapse.config import ConfigError
from synapse.config.captcha import CaptchaConfig
from synapse.config.consent_config import ConsentConfig
from synapse.config.emailconfig import ThreepidBehaviour from synapse.config.emailconfig import ThreepidBehaviour
from synapse.config.ratelimiting import FederationRateLimitConfig from synapse.config.ratelimiting import FederationRateLimitConfig
from synapse.config.registration import RegistrationConfig
from synapse.config.server import is_threepid_reserved from synapse.config.server import is_threepid_reserved
from synapse.handlers.auth import AuthHandler
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,
@ -246,6 +252,12 @@ class RegistrationSubmitTokenServlet(RestServlet):
[self.config.email_registration_template_failure_html], [self.config.email_registration_template_failure_html],
) )
if self.config.threepid_behaviour_email == ThreepidBehaviour.LOCAL:
self.failure_email_template, = load_jinja2_templates(
self.config.email_template_dir,
[self.config.email_registration_template_failure_html],
)
@defer.inlineCallbacks @defer.inlineCallbacks
def on_GET(self, request, medium): def on_GET(self, request, medium):
if medium != "email": if medium != "email":
@ -365,6 +377,10 @@ class RegisterRestServlet(RestServlet):
self.ratelimiter = hs.get_registration_ratelimiter() self.ratelimiter = hs.get_registration_ratelimiter()
self.clock = hs.get_clock() self.clock = hs.get_clock()
self._registration_flows = _calculate_registration_flows(
hs.config, self.auth_handler
)
@interactive_auth_handler @interactive_auth_handler
@defer.inlineCallbacks @defer.inlineCallbacks
def on_POST(self, request): def on_POST(self, request):
@ -485,69 +501,8 @@ class RegisterRestServlet(RestServlet):
assigned_user_id=registered_user_id, assigned_user_id=registered_user_id,
) )
# FIXME: need a better error than "no auth flow found" for scenarios
# where we required 3PID for registration but the user didn't give one
require_email = "email" in self.hs.config.registrations_require_3pid
require_msisdn = "msisdn" in self.hs.config.registrations_require_3pid
show_msisdn = True
if self.hs.config.disable_msisdn_registration:
show_msisdn = False
require_msisdn = False
flows = []
if self.hs.config.enable_registration_captcha:
# only support 3PIDless registration if no 3PIDs are required
if not require_email and not require_msisdn:
# Also add a dummy flow here, otherwise if a client completes
# recaptcha first we'll assume they were going for this flow
# and complete the request, when they could have been trying to
# complete one of the flows with email/msisdn auth.
flows.extend([[LoginType.RECAPTCHA, LoginType.DUMMY]])
# only support the email-only flow if we don't require MSISDN 3PIDs
if not require_msisdn:
flows.extend([[LoginType.RECAPTCHA, LoginType.EMAIL_IDENTITY]])
if show_msisdn:
# only support the MSISDN-only flow if we don't require email 3PIDs
if not require_email:
flows.extend([[LoginType.RECAPTCHA, LoginType.MSISDN]])
# always let users provide both MSISDN & email
flows.extend(
[[LoginType.RECAPTCHA, LoginType.MSISDN, LoginType.EMAIL_IDENTITY]]
)
else:
# only support 3PIDless registration if no 3PIDs are required
if not require_email and not require_msisdn:
flows.extend([[LoginType.DUMMY]])
# only support the email-only flow if we don't require MSISDN 3PIDs
if not require_msisdn:
flows.extend([[LoginType.EMAIL_IDENTITY]])
if show_msisdn:
# only support the MSISDN-only flow if we don't require email 3PIDs
if not require_email or require_msisdn:
flows.extend([[LoginType.MSISDN]])
# always let users provide both MSISDN & email
flows.extend([[LoginType.MSISDN, LoginType.EMAIL_IDENTITY]])
# Append m.login.terms to all flows if we're requiring consent
if self.hs.config.user_consent_at_registration:
new_flows = []
for flow in flows:
inserted = False
# m.login.terms should go near the end but before msisdn or email auth
for i, stage in enumerate(flow):
if stage == LoginType.EMAIL_IDENTITY or stage == LoginType.MSISDN:
flow.insert(i, LoginType.TERMS)
inserted = True
break
if not inserted:
flow.append(LoginType.TERMS)
flows.extend(new_flows)
auth_result, params, session_id = yield self.auth_handler.check_auth( auth_result, params, session_id = yield self.auth_handler.check_auth(
flows, body, self.hs.get_ip_from_request(request) self._registration_flows, body, self.hs.get_ip_from_request(request)
) )
# Check that we're not trying to register a denied 3pid. # Check that we're not trying to register a denied 3pid.
@ -710,6 +665,83 @@ class RegisterRestServlet(RestServlet):
) )
def _calculate_registration_flows(
# technically `config` has to provide *all* of these interfaces, not just one
config: Union[RegistrationConfig, ConsentConfig, CaptchaConfig],
auth_handler: AuthHandler,
) -> List[List[str]]:
"""Get a suitable flows list for registration
Args:
config: server configuration
auth_handler: authorization handler
Returns: a list of supported flows
"""
# FIXME: need a better error than "no auth flow found" for scenarios
# where we required 3PID for registration but the user didn't give one
require_email = "email" in config.registrations_require_3pid
require_msisdn = "msisdn" in config.registrations_require_3pid
show_msisdn = True
show_email = True
if config.disable_msisdn_registration:
show_msisdn = False
require_msisdn = False
enabled_auth_types = auth_handler.get_enabled_auth_types()
if LoginType.EMAIL_IDENTITY not in enabled_auth_types:
show_email = False
if require_email:
raise ConfigError(
"Configuration requires email address at registration, but email "
"validation is not configured"
)
if LoginType.MSISDN not in enabled_auth_types:
show_msisdn = False
if require_msisdn:
raise ConfigError(
"Configuration requires msisdn at registration, but msisdn "
"validation is not configured"
)
flows = []
# only support 3PIDless registration if no 3PIDs are required
if not require_email and not require_msisdn:
# Add a dummy step here, otherwise if a client completes
# recaptcha first we'll assume they were going for this flow
# and complete the request, when they could have been trying to
# complete one of the flows with email/msisdn auth.
flows.append([LoginType.DUMMY])
# only support the email-only flow if we don't require MSISDN 3PIDs
if show_email and not require_msisdn:
flows.append([LoginType.EMAIL_IDENTITY])
# only support the MSISDN-only flow if we don't require email 3PIDs
if show_msisdn and not require_email:
flows.append([LoginType.MSISDN])
if show_email and show_msisdn:
# always let users provide both MSISDN & email
flows.append([LoginType.MSISDN, LoginType.EMAIL_IDENTITY])
# Prepend m.login.terms to all flows if we're requiring consent
if config.user_consent_at_registration:
for flow in flows:
flow.insert(0, LoginType.TERMS)
# Prepend recaptcha to all flows if we're requiring captcha
if config.enable_registration_captcha:
for flow in flows:
flow.insert(0, LoginType.RECAPTCHA)
return flows
def register_servlets(hs, http_server): 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)

View File

@ -48,7 +48,24 @@ class VersionsRestServlet(RestServlet):
"r0.5.0", "r0.5.0",
], ],
# as per MSC1497: # as per MSC1497:
"unstable_features": {"m.lazy_load_members": True}, "unstable_features": {
"m.lazy_load_members": True,
# as per MSC2190, as amended by MSC2264
# to be removed in r0.6.0
"m.id_access_token": True,
# Advertise to clients that they need not include an `id_server`
# parameter during registration or password reset, as Synapse now decides
# itself which identity server to use (or none at all).
#
# This is also used by a client when they wish to bind a 3PID to their
# account, but not bind it to an identity server, the endpoint for which
# also requires `id_server`. If the homeserver is handling 3PID
# verification itself, there is no need to ask the user for `id_server` to
# be supplied.
"m.require_identity_server": False,
# as per MSC2290
"m.separate_add_and_bind": True,
},
}, },
) )

View File

@ -238,7 +238,7 @@ class BackgroundUpdateStore(SQLBaseStore):
duration_ms = time_stop - time_start duration_ms = time_stop - time_start
logger.info( logger.info(
"Updating %r. Updated %r items in %rms." "Running background update %r. Processed %r items in %rms."
" (total_rate=%r/ms, current_rate=%r/ms, total_updated=%r, batch_size=%r)", " (total_rate=%r/ms, current_rate=%r/ms, total_updated=%r, batch_size=%r)",
update_name, update_name,
items_updated, items_updated,

View File

@ -397,7 +397,7 @@ class ClientIpStore(background_updates.BackgroundUpdateStore):
""" """
keyvalues = {"user_id": user_id} keyvalues = {"user_id": user_id}
if device_id: if device_id is not None:
keyvalues["device_id"] = device_id keyvalues["device_id"] = device_id
res = yield self._simple_select_list( res = yield self._simple_select_list(

View File

@ -22,6 +22,7 @@ from six import iterkeys
from six.moves import range from six.moves import range
from twisted.internet import defer from twisted.internet import defer
from twisted.internet.defer import Deferred
from synapse.api.constants import UserTypes from synapse.api.constants import UserTypes
from synapse.api.errors import Codes, StoreError, SynapseError, ThreepidValidationError from synapse.api.errors import Codes, StoreError, SynapseError, ThreepidValidationError
@ -384,6 +385,26 @@ class RegistrationWorkerStore(SQLBaseStore):
return self.runInteraction("get_users_by_id_case_insensitive", f) return self.runInteraction("get_users_by_id_case_insensitive", f)
async def get_user_by_external_id(
self, auth_provider: str, external_id: str
) -> str:
"""Look up a user by their external auth id
Args:
auth_provider: identifier for the remote auth provider
external_id: id on that system
Returns:
str|None: the mxid of the user, or None if they are not known
"""
return await self._simple_select_one_onecol(
table="user_external_ids",
keyvalues={"auth_provider": auth_provider, "external_id": external_id},
retcol="user_id",
allow_none=True,
desc="get_user_by_external_id",
)
@defer.inlineCallbacks @defer.inlineCallbacks
def count_all_users(self): def count_all_users(self):
"""Counts all users registered on the homeserver.""" """Counts all users registered on the homeserver."""
@ -495,7 +516,7 @@ class RegistrationWorkerStore(SQLBaseStore):
) )
@defer.inlineCallbacks @defer.inlineCallbacks
def get_user_id_by_threepid(self, medium, address, require_verified=False): def get_user_id_by_threepid(self, medium, address):
"""Returns user id from threepid """Returns user id from threepid
Args: Args:
@ -586,6 +607,26 @@ class RegistrationWorkerStore(SQLBaseStore):
desc="add_user_bound_threepid", desc="add_user_bound_threepid",
) )
def user_get_bound_threepids(self, user_id):
"""Get the threepids that a user has bound to an identity server through the homeserver
The homeserver remembers where binds to an identity server occurred. Using this
method can retrieve those threepids.
Args:
user_id (str): The ID of the user to retrieve threepids for
Returns:
Deferred[list[dict]]: List of dictionaries containing the following:
medium (str): The medium of the threepid (e.g "email")
address (str): The address of the threepid (e.g "bob@example.com")
"""
return self._simple_select_list(
table="user_threepid_id_server",
keyvalues={"user_id": user_id},
retcols=["medium", "address"],
desc="user_get_bound_threepids",
)
def remove_user_bound_threepid(self, user_id, medium, address, id_server): def remove_user_bound_threepid(self, user_id, medium, address, id_server):
"""The server proxied an unbind request to the given identity server on """The server proxied an unbind request to the given identity server on
behalf of the given user, so we remove the mapping of threepid to behalf of the given user, so we remove the mapping of threepid to
@ -655,7 +696,7 @@ class RegistrationWorkerStore(SQLBaseStore):
self, medium, client_secret, address=None, sid=None, validated=True self, medium, client_secret, address=None, sid=None, validated=True
): ):
"""Gets a session_id and last_send_attempt (if available) for a """Gets a session_id and last_send_attempt (if available) for a
client_secret/medium/(address|session_id) combo combination of validation metadata
Args: Args:
medium (str|None): The medium of the 3PID medium (str|None): The medium of the 3PID
@ -824,7 +865,7 @@ class RegistrationStore(
rows = self.cursor_to_dict(txn) rows = self.cursor_to_dict(txn)
if not rows: if not rows:
return True return True, 0
rows_processed_nb = 0 rows_processed_nb = 0
@ -840,18 +881,18 @@ class RegistrationStore(
) )
if batch_size > len(rows): if batch_size > len(rows):
return True return True, len(rows)
else: else:
return False return False, len(rows)
end = yield self.runInteraction( end, nb_processed = yield self.runInteraction(
"users_set_deactivated_flag", _background_update_set_deactivated_flag_txn "users_set_deactivated_flag", _background_update_set_deactivated_flag_txn
) )
if end: if end:
yield self._end_background_update("users_set_deactivated_flag") yield self._end_background_update("users_set_deactivated_flag")
return batch_size return nb_processed
@defer.inlineCallbacks @defer.inlineCallbacks
def add_access_token_to_user(self, user_id, token, device_id, valid_until_ms): def add_access_token_to_user(self, user_id, token, device_id, valid_until_ms):
@ -1012,6 +1053,26 @@ class RegistrationStore(
self._invalidate_cache_and_stream(txn, self.get_user_by_id, (user_id,)) self._invalidate_cache_and_stream(txn, self.get_user_by_id, (user_id,))
txn.call_after(self.is_guest.invalidate, (user_id,)) txn.call_after(self.is_guest.invalidate, (user_id,))
def record_user_external_id(
self, auth_provider: str, external_id: str, user_id: str
) -> Deferred:
"""Record a mapping from an external user id to a mxid
Args:
auth_provider: identifier for the remote auth provider
external_id: id on that system
user_id: complete mxid that it is mapped to
"""
return self._simple_insert(
table="user_external_ids",
values={
"auth_provider": auth_provider,
"external_id": external_id,
"user_id": user_id,
},
desc="record_user_external_id",
)
def user_set_password_hash(self, user_id, password_hash): def user_set_password_hash(self, user_id, password_hash):
""" """
NB. This does *not* evict any cache because the one use for this NB. This does *not* evict any cache because the one use for this

View File

@ -0,0 +1,24 @@
/* Copyright 2019 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/*
* a table which records mappings from external auth providers to mxids
*/
CREATE TABLE IF NOT EXISTS user_external_ids (
auth_provider TEXT NOT NULL,
external_id TEXT NOT NULL,
user_id TEXT NOT NULL,
UNIQUE (auth_provider, external_id)
);

View File

@ -14,12 +14,13 @@
# limitations under the License. # limitations under the License.
import importlib import importlib
import importlib.util
from synapse.config._base import ConfigError from synapse.config._base import ConfigError
def load_module(provider): def load_module(provider):
""" Loads a module with its config """ Loads a synapse module with its config
Take a dict with keys 'module' (the module name) and 'config' Take a dict with keys 'module' (the module name) and 'config'
(the config dict). (the config dict).
@ -38,3 +39,20 @@ def load_module(provider):
raise ConfigError("Failed to parse config for %r: %r" % (provider["module"], e)) raise ConfigError("Failed to parse config for %r: %r" % (provider["module"], e))
return provider_class, provider_config return provider_class, provider_config
def load_python_module(location: str):
"""Load a python module, and return a reference to its global namespace
Args:
location (str): path to the module
Returns:
python module object
"""
spec = importlib.util.spec_from_file_location(location, location)
if spec is None:
raise Exception("Unable to load module at %s" % (location,))
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
return mod

View File

@ -29,12 +29,3 @@ Enabling an unknown default rule fails with 404
# Blacklisted due to https://github.com/matrix-org/synapse/issues/1663 # Blacklisted due to https://github.com/matrix-org/synapse/issues/1663
New federated private chats get full presence information (SYN-115) New federated private chats get full presence information (SYN-115)
# Blacklisted temporarily due to https://github.com/matrix-org/matrix-doc/pull/2290
# These sytests need to be updated with new endpoints, which will come in a later PR
# That PR will also remove this blacklist
Can bind 3PID via home server
Can bind and unbind 3PID via homeserver
3PIDs are unbound after account deactivation
Can bind and unbind 3PID via /unbind by specifying the identity server
Can bind and unbind 3PID via /unbind without specifying the identity server

View File

@ -18,11 +18,22 @@ from twisted.internet.defer import succeed
import synapse.rest.admin import synapse.rest.admin
from synapse.api.constants import LoginType from synapse.api.constants import LoginType
from synapse.handlers.ui_auth.checkers import UserInteractiveAuthChecker
from synapse.rest.client.v2_alpha import auth, register from synapse.rest.client.v2_alpha import auth, register
from tests import unittest from tests import unittest
class DummyRecaptchaChecker(UserInteractiveAuthChecker):
def __init__(self, hs):
super().__init__(hs)
self.recaptcha_attempts = []
def check_auth(self, authdict, clientip):
self.recaptcha_attempts.append((authdict, clientip))
return succeed(True)
class FallbackAuthTests(unittest.HomeserverTestCase): class FallbackAuthTests(unittest.HomeserverTestCase):
servlets = [ servlets = [
@ -44,15 +55,9 @@ class FallbackAuthTests(unittest.HomeserverTestCase):
return hs return hs
def prepare(self, reactor, clock, hs): def prepare(self, reactor, clock, hs):
self.recaptcha_checker = DummyRecaptchaChecker(hs)
auth_handler = hs.get_auth_handler() auth_handler = hs.get_auth_handler()
auth_handler.checkers[LoginType.RECAPTCHA] = self.recaptcha_checker
self.recaptcha_attempts = []
def _recaptcha(authdict, clientip):
self.recaptcha_attempts.append((authdict, clientip))
return succeed(True)
auth_handler.checkers[LoginType.RECAPTCHA] = _recaptcha
@unittest.INFO @unittest.INFO
def test_fallback_captcha(self): def test_fallback_captcha(self):
@ -89,8 +94,9 @@ class FallbackAuthTests(unittest.HomeserverTestCase):
self.assertEqual(request.code, 200) self.assertEqual(request.code, 200)
# The recaptcha handler is called with the response given # The recaptcha handler is called with the response given
self.assertEqual(len(self.recaptcha_attempts), 1) attempts = self.recaptcha_checker.recaptcha_attempts
self.assertEqual(self.recaptcha_attempts[0][0]["response"], "a") self.assertEqual(len(attempts), 1)
self.assertEqual(attempts[0][0]["response"], "a")
# also complete the dummy auth # also complete the dummy auth
request, channel = self.make_request( request, channel = self.make_request(

View File

@ -34,19 +34,12 @@ from tests import unittest
class RegisterRestServletTestCase(unittest.HomeserverTestCase): class RegisterRestServletTestCase(unittest.HomeserverTestCase):
servlets = [register.register_servlets] servlets = [register.register_servlets]
url = b"/_matrix/client/r0/register"
def make_homeserver(self, reactor, clock): def default_config(self, name="test"):
config = super().default_config(name)
self.url = b"/_matrix/client/r0/register" config["allow_guest_access"] = True
return config
self.hs = self.setup_test_homeserver()
self.hs.config.enable_registration = True
self.hs.config.registrations_require_3pid = []
self.hs.config.auto_join_rooms = []
self.hs.config.enable_registration_captcha = False
self.hs.config.allow_guest_access = True
return self.hs
def test_POST_appservice_registration_valid(self): def test_POST_appservice_registration_valid(self):
user_id = "@as_user_kermit:test" user_id = "@as_user_kermit:test"
@ -199,6 +192,73 @@ class RegisterRestServletTestCase(unittest.HomeserverTestCase):
self.assertEquals(channel.result["code"], b"200", channel.result) self.assertEquals(channel.result["code"], b"200", channel.result)
def test_advertised_flows(self):
request, channel = self.make_request(b"POST", self.url, b"{}")
self.render(request)
self.assertEquals(channel.result["code"], b"401", channel.result)
flows = channel.json_body["flows"]
# with the stock config, we only expect the dummy flow
self.assertCountEqual([["m.login.dummy"]], (f["stages"] for f in flows))
@unittest.override_config(
{
"enable_registration_captcha": True,
"user_consent": {
"version": "1",
"template_dir": "/",
"require_at_registration": True,
},
"account_threepid_delegates": {
"email": "https://id_server",
"msisdn": "https://id_server",
},
}
)
def test_advertised_flows_captcha_and_terms_and_3pids(self):
request, channel = self.make_request(b"POST", self.url, b"{}")
self.render(request)
self.assertEquals(channel.result["code"], b"401", channel.result)
flows = channel.json_body["flows"]
self.assertCountEqual(
[
["m.login.recaptcha", "m.login.terms", "m.login.dummy"],
["m.login.recaptcha", "m.login.terms", "m.login.email.identity"],
["m.login.recaptcha", "m.login.terms", "m.login.msisdn"],
[
"m.login.recaptcha",
"m.login.terms",
"m.login.msisdn",
"m.login.email.identity",
],
],
(f["stages"] for f in flows),
)
@unittest.override_config(
{
"public_baseurl": "https://test_server",
"registrations_require_3pid": ["email"],
"disable_msisdn_registration": True,
"email": {
"smtp_host": "mail_server",
"smtp_port": 2525,
"notif_from": "sender@host",
},
}
)
def test_advertised_flows_no_msisdn_email_required(self):
request, channel = self.make_request(b"POST", self.url, b"{}")
self.render(request)
self.assertEquals(channel.result["code"], b"401", channel.result)
flows = channel.json_body["flows"]
# with the stock config, we expect all four combinations of 3pid
self.assertCountEqual(
[["m.login.email.identity"]], (f["stages"] for f in flows)
)
class AccountValidityTestCase(unittest.HomeserverTestCase): class AccountValidityTestCase(unittest.HomeserverTestCase):

View File

@ -28,6 +28,21 @@ from tests import unittest
class TermsTestCase(unittest.HomeserverTestCase): class TermsTestCase(unittest.HomeserverTestCase):
servlets = [register_servlets] servlets = [register_servlets]
def default_config(self, name="test"):
config = super().default_config(name)
config.update(
{
"public_baseurl": "https://example.org/",
"user_consent": {
"version": "1.0",
"policy_name": "My Cool Privacy Policy",
"template_dir": "/",
"require_at_registration": True,
},
}
)
return config
def prepare(self, reactor, clock, hs): def prepare(self, reactor, clock, hs):
self.clock = MemoryReactorClock() self.clock = MemoryReactorClock()
self.hs_clock = Clock(self.clock) self.hs_clock = Clock(self.clock)
@ -35,17 +50,8 @@ class TermsTestCase(unittest.HomeserverTestCase):
self.registration_handler = Mock() self.registration_handler = Mock()
self.auth_handler = Mock() self.auth_handler = Mock()
self.device_handler = Mock() self.device_handler = Mock()
hs.config.enable_registration = True
hs.config.registrations_require_3pid = []
hs.config.auto_join_rooms = []
hs.config.enable_registration_captcha = False
def test_ui_auth(self): def test_ui_auth(self):
self.hs.config.user_consent_at_registration = True
self.hs.config.user_consent_policy_name = "My Cool Privacy Policy"
self.hs.config.public_baseurl = "https://example.org/"
self.hs.config.user_consent_version = "1.0"
# Do a UI auth request # Do a UI auth request
request, channel = self.make_request(b"POST", self.url, b"{}") request, channel = self.make_request(b"POST", self.url, b"{}")
self.render(request) self.render(request)