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

Add config option to selectively delegate different operations to account threepid delegates #7611

Closed
wants to merge 10 commits into from
Closed
1 change: 1 addition & 0 deletions changelog.d/7611.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add a `delegate_for` configuration flag to `account_threepid_delegates` to allow selectively delegating certain 3PID validation services.
16 changes: 13 additions & 3 deletions docs/sample_config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1160,9 +1160,9 @@ account_validity:
# - matrix.org
# - vector.im

# Handle threepid (email/phone etc) registration and password resets through a set of
# *trusted* identity servers. Note that this allows the configured identity server to
# reset passwords for accounts!
# Handle threepid (email/phone etc) registration and/or password resets through a set of
# *trusted* identity servers. Note that this potentially allows the configured external
# service to reset passwords for accounts!
#
# Be aware that if `email` is not set, and SMTP options have not been
# configured in the email config block, registration and user password resets via
Expand All @@ -1186,6 +1186,16 @@ account_threepid_delegates:
#email: https://example.com # Delegate email sending to example.com
#msisdn: http://localhost:8090 # Delegate SMS sending to this local process

# Choose which services are delegated. These apply to both email and msisdn
# identities.
#
# * adding_threepid: when adding an email or phone number to your homeserver
# * password_resets: when verifying your email or phone number during password reset
#
# Default is delegating for all services, or [adding_threepid, password_resets].
#
#delegate_for: [adding_threepid]

# Whether users are allowed to change their displayname after it has
# been initially set. Useful when provisioning users based on the
# contents of a third-party directory.
Expand Down
269 changes: 176 additions & 93 deletions synapse/config/emailconfig.py

Large diffs are not rendered by default.

31 changes: 28 additions & 3 deletions synapse/config/registration.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import pkg_resources

from synapse.config._base import Config, ConfigError
from synapse.config.emailconfig import ThreepidService
from synapse.types import RoomAlias
from synapse.util.stringutils import random_string_with_symbols

Expand Down Expand Up @@ -117,6 +118,20 @@ def read_config(self, config, **kwargs):
"configure `public_baseurl`."
)

delegate_for = account_threepid_delegates.get(
"delegate_for",
[ThreepidService.ADDING_THREEPID, ThreepidService.PASSWORD_RESET],
)
try:
self.account_threepid_delegate_delegate_for = [
ThreepidService(service_type) for service_type in delegate_for
]
except ValueError as e:
# This will be raised if a provided service_type does not exist
raise ConfigError(
"Option account_threepid_delegates.delegate_for: %s" % (e,)
)

self.default_identity_server = config.get("default_identity_server")
self.allow_guest_access = config.get("allow_guest_access", False)

Expand Down Expand Up @@ -307,9 +322,9 @@ def generate_config_section(self, generate_secrets=False, **kwargs):
# - matrix.org
# - vector.im

# Handle threepid (email/phone etc) registration and password resets through a set of
# *trusted* identity servers. Note that this allows the configured identity server to
# reset passwords for accounts!
# Handle threepid (email/phone etc) registration and/or password resets through a set of
# *trusted* identity servers. Note that this potentially allows the configured external
# service to reset passwords for accounts!
#
# Be aware that if `email` is not set, and SMTP options have not been
# configured in the email config block, registration and user password resets via
Expand All @@ -333,6 +348,16 @@ def generate_config_section(self, generate_secrets=False, **kwargs):
#email: https://example.com # Delegate email sending to example.com
#msisdn: http://localhost:8090 # Delegate SMS sending to this local process

# Choose which services are delegated. These apply to both email and msisdn
# identities.
#
# * adding_threepid: when adding an email or phone number to your homeserver
# * password_resets: when verifying your email or phone number during password reset
#
# Default is delegating for all services, or [adding_threepid, password_resets].
#
#delegate_for: [adding_threepid]

# Whether users are allowed to change their displayname after it has
# been initially set. Useful when provisioning users based on the
# contents of a third-party directory.
Expand Down
38 changes: 31 additions & 7 deletions synapse/handlers/identity.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

import logging
import urllib.parse
from typing import Dict, Optional, Union

from canonicaljson import json
from signedjson.key import decode_verify_key_bytes
Expand All @@ -34,7 +35,7 @@
HttpResponseException,
SynapseError,
)
from synapse.config.emailconfig import ThreepidBehaviour
from synapse.config.emailconfig import ThreepidBehaviour, ThreepidService # noqa: F401
from synapse.http.client import SimpleHttpClient
from synapse.util.hash import sha256_and_url_safe_base64
from synapse.util.stringutils import assert_valid_client_secret, random_string
Expand All @@ -59,6 +60,10 @@ def __init__(self, hs):
self.federation_http_client = hs.get_http_client()
self.hs = hs

self._threepid_behaviour_email_add_threepid = (
self.hs.config.threepid_behaviour_email_add_threepid
)

async def threepid_from_creds(self, id_server, creds):
"""
Retrieve and validate a threepid identifier from a "credentials" dictionary against a
Expand Down Expand Up @@ -291,6 +296,7 @@ async def send_threepid_validation(
client_secret,
send_attempt,
send_email_func,
service,
next_link=None,
):
"""Send a threepid validation email for password reset or
Expand All @@ -303,6 +309,8 @@ async def send_threepid_validation(
send_email_func (func): A function that takes an email address, token,
client_secret and session_id, sends an email
and returns a Deferred.
service (ThreepidService): The type of threepid service that
this validation session will authorise
next_link (str|None): The URL to redirect the user to after validation

Returns:
Expand Down Expand Up @@ -367,6 +375,7 @@ async def send_threepid_validation(
next_link,
token,
token_expires,
service,
)

return session_id
Expand Down Expand Up @@ -480,17 +489,27 @@ async def requestMsisdnToken(
)
return data

async def validate_threepid_session(self, client_secret, sid):
async def validate_threepid_session(
self, client_secret: str, sid: str
) -> Optional[Dict[str, Union[str, int]]]:
"""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
client_secret: A secret provided by the client

sid (str): The ID of the session
sid: The ID of the session

Returns:
Dict[str, str|int] if validation was successful, otherwise None
If validation succeeds, a dict containing:
* address - address of the 3pid
* medium - medium of the 3pid
* client_secret - a secret provided by the client for this validation session
* session_id - ID of the validation session
* send_attempt - a number serving to dedupe send attempts for this session
* validated_at - timestamp of when this session was validated if so

Otherwise None if a validation session is not found
"""
# XXX: We shouldn't need to keep wrapping and unwrapping this value
threepid_creds = {"client_secret": client_secret, "sid": sid}
Expand All @@ -500,12 +519,17 @@ async def validate_threepid_session(self, client_secret, sid):
validation_session = None

# Try to validate as email
if self.hs.config.threepid_behaviour_email == ThreepidBehaviour.REMOTE:

# Note that this method is only used by /register and /account/3pid* currently,
# so there's no need to consult config.threepid_behaviour_email_password_reset
threepid_behaviour = self._threepid_behaviour_email_add_threepid

if threepid_behaviour == ThreepidBehaviour.REMOTE:
# Ask our delegated email identity server
validation_session = await self.threepid_from_creds(
self.hs.config.account_threepid_delegate_email, threepid_creds
)
elif self.hs.config.threepid_behaviour_email == ThreepidBehaviour.LOCAL:
elif threepid_behaviour == ThreepidBehaviour.LOCAL:
# Get a validated session matching these details
validation_session = await self.store.get_threepid_validation_session(
"email", client_secret, sid=sid, validated=True
Expand Down
72 changes: 63 additions & 9 deletions synapse/handlers/ui_auth/checkers.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import logging
from typing import Any, Dict, Optional

from canonicaljson import json

Expand All @@ -21,7 +22,7 @@

from synapse.api.constants import LoginType
from synapse.api.errors import Codes, LoginError, SynapseError
from synapse.config.emailconfig import ThreepidBehaviour
from synapse.config.emailconfig import ThreepidBehaviour, ThreepidService

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -138,7 +139,21 @@ def __init__(self, hs):
self.hs = hs
self.store = hs.get_datastore()

async def _check_threepid(self, medium, authdict):
self._account_threepid_delegate_email = (
self.hs.config.account_threepid_delegate_email
)
self._account_threepid_delegate_msisdn = (
self.hs.config.account_threepid_delegate_msisdn
)

self._threepid_behaviour_email_add_threepid = (
self.hs.config.threepid_behaviour_email_add_threepid
)
self._threepid_behaviour_email_password_reset = (
self.hs.config.threepid_behaviour_email_password_reset
)

async def _check_threepid(self, medium: str, authdict: Dict[str, Any]):
if "threepid_creds" not in authdict:
raise LoginError(400, "Missing threepid_creds", Codes.MISSING_PARAM)

Expand All @@ -150,20 +165,54 @@ async def _check_threepid(self, medium, authdict):

# msisdns are currently always ThreepidBehaviour.REMOTE
if medium == "msisdn":
if not self.hs.config.account_threepid_delegate_msisdn:
if not self._account_threepid_delegate_msisdn:
raise SynapseError(
400, "Phone number verification is not enabled on this homeserver"
)
threepid = await identity_handler.threepid_from_creds(
self.hs.config.account_threepid_delegate_msisdn, threepid_creds
self._account_threepid_delegate_msisdn, threepid_creds
)
elif medium == "email":
if self.hs.config.threepid_behaviour_email == ThreepidBehaviour.REMOTE:
# Determine why this check is happening. This will help us decide whether
# Synapse or an account threepid delegate should complete the validation
service = None # type: Optional[ThreepidService]

session = await self.store.get_threepid_validation_session(
medium,
threepid_creds["client_secret"],
sid=threepid_creds["sid"],
validated=True,
)

if session:
# We found a local session.
# Determine which service this was intended to authorise
service = ThreepidService(session["service"])

if service == ThreepidService.ADDING_THREEPID:
threepid_behaviour = self._threepid_behaviour_email_add_threepid
elif service == ThreepidService.PASSWORD_RESET:
threepid_behaviour = self._threepid_behaviour_email_password_reset
else:
raise SynapseError(500, "Unknown threepid service")
else:
# We can't find a local, matching session.
# Do we have a threepid delegate configured?
if not self.hs.config.account_threepid_delegate_email:
raise LoginError(401, "", errcode=Codes.UNAUTHORIZED)

# We do. Presume that this is a remote session
threepid_behaviour = ThreepidBehaviour.REMOTE

if threepid_behaviour == ThreepidBehaviour.REMOTE:
assert self.hs.config.account_threepid_delegate_email

# Ask our threepid delegate about this validation attempt
threepid = await identity_handler.threepid_from_creds(
self.hs.config.account_threepid_delegate_email, threepid_creds
)
elif self.hs.config.threepid_behaviour_email == ThreepidBehaviour.LOCAL:
elif threepid_behaviour == ThreepidBehaviour.LOCAL:
# Attempt to validate locally
threepid = None
row = await self.store.get_threepid_validation_session(
medium,
Expand Down Expand Up @@ -213,10 +262,15 @@ def __init__(self, hs):
_BaseThreepidAuthChecker.__init__(self, hs)

def is_enabled(self):
return self.hs.config.threepid_behaviour_email in (
ThreepidBehaviour.REMOTE,
ThreepidBehaviour.LOCAL,
add_threepid_enabled = (
self.hs.config.threepid_behaviour_email_add_threepid
!= ThreepidBehaviour.OFF
)
password_reset_enabled = (
self.hs.config.threepid_behaviour_email_password_reset
!= ThreepidBehaviour.OFF
)
return add_threepid_enabled or password_reset_enabled

def check_auth(self, authdict, clientip):
return defer.ensureDeferred(self._check_threepid("email", authdict))
Expand Down
Loading