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

Commit

Permalink
Add option to autobind user's email on registration (#51)
Browse files Browse the repository at this point in the history
Adds an option, `bind_new_user_emails_to_sydent`, which uses Sydent's [internal bind api](https://github.com/matrix-org/sydent#internal-bind-and-unbind-api) to automatically bind email addresses of users immediately after they register.

This is quite enterprise-specific, but could be generally useful to multiple organizations. This aims to solve the problem of requiring users to verify their email twice when using the functionality of an identity server in a corporate deployment - where both the homeserver and identity server are controlled. It does with while eliminating the need for the `account_threepid_delegates.email` option, which historically has been a very complicated option to reason about.
  • Loading branch information
anoadragon453 authored Jul 2, 2020
1 parent 4200f54 commit 21821c0
Show file tree
Hide file tree
Showing 6 changed files with 183 additions and 25 deletions.
1 change: 1 addition & 0 deletions changelog.d/51.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add `bind_new_user_emails_to_sydent` option for automatically binding user's emails after registration.
18 changes: 18 additions & 0 deletions docs/sample_config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1387,6 +1387,24 @@ account_threepid_delegates:
#rewrite_identity_server_urls:
# "https://somewhere.example.com": "https://somewhereelse.example.com"

# When a user registers an account with an email address, it can be useful to
# bind that email address to their mxid on an identity server. Typically, this
# requires the user to validate their email address with the identity server.
# However if Synapse itself is handling email validation on registration, the
# user ends up needing to validate their email twice, which leads to poor UX.
#
# It is possible to force Sydent, one identity server implementation, to bind
# threepids using its internal, unauthenticated bind API:
# https://github.com/matrix-org/sydent/#internal-bind-and-unbind-api
#
# Configure the address of a Sydent server here to have Synapse attempt
# to automatically bind users' emails following registration. The
# internal bind API must be reachable from Synapse, but should NOT be
# exposed to any third party, as it allows the creation of bindings
# without validation.
#
#bind_new_user_emails_to_sydent: https://example.com:8091


## Metrics ###

Expand Down
35 changes: 35 additions & 0 deletions synapse/config/registration.py
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,23 @@ def read_config(self, config, **kwargs):
session_lifetime = self.parse_duration(session_lifetime)
self.session_lifetime = session_lifetime

self.bind_new_user_emails_to_sydent = config.get(
"bind_new_user_emails_to_sydent"
)

if self.bind_new_user_emails_to_sydent:
if not isinstance(
self.bind_new_user_emails_to_sydent, str
) or not self.bind_new_user_emails_to_sydent.startswith("http"):
raise ConfigError(
"Option bind_new_user_emails_to_sydent has invalid value"
)

# Remove trailing slashes
self.bind_new_user_emails_to_sydent = self.bind_new_user_emails_to_sydent.strip(
"/"
)

def generate_config_section(self, generate_secrets=False, **kwargs):
if generate_secrets:
registration_shared_secret = 'registration_shared_secret: "%s"' % (
Expand Down Expand Up @@ -469,6 +486,24 @@ def generate_config_section(self, generate_secrets=False, **kwargs):
#
#rewrite_identity_server_urls:
# "https://somewhere.example.com": "https://somewhereelse.example.com"
# When a user registers an account with an email address, it can be useful to
# bind that email address to their mxid on an identity server. Typically, this
# requires the user to validate their email address with the identity server.
# However if Synapse itself is handling email validation on registration, the
# user ends up needing to validate their email twice, which leads to poor UX.
#
# It is possible to force Sydent, one identity server implementation, to bind
# threepids using its internal, unauthenticated bind API:
# https://github.com/matrix-org/sydent/#internal-bind-and-unbind-api
#
# Configure the address of a Sydent server here to have Synapse attempt
# to automatically bind users' emails following registration. The
# internal bind API must be reachable from Synapse, but should NOT be
# exposed to any third party, as it allows the creation of bindings
# without validation.
#
#bind_new_user_emails_to_sydent: https://example.com:8091
"""
% locals()
)
Expand Down
24 changes: 24 additions & 0 deletions synapse/handlers/identity.py
Original file line number Diff line number Diff line change
Expand Up @@ -1033,6 +1033,30 @@ async def ask_id_server_for_third_party_invite(
display_name = data["display_name"]
return token, public_keys, fallback_public_key, display_name

async def bind_email_using_internal_sydent_api(
self, id_server_url: str, email: str, user_id: str,
):
"""Bind an email to a fully qualified user ID using the internal API of an
instance of Sydent.
Args:
id_server_url: The URL of the Sydent instance
email: The email address to bind
user_id: The user ID to bind the email to
Raises:
HTTPResponseException: On a non-2xx HTTP response.
"""
# id_server_url is assumed to have no trailing slashes
url = id_server_url + "/_matrix/identity/internal/bind"
body = {
"address": email,
"medium": "email",
"mxid": user_id,
}

await self.http_client.post_json_get_json(url, body)


def create_id_access_token_header(id_access_token):
"""Create an Authorization header for passing to SimpleHttpClient as the header value
Expand Down
45 changes: 21 additions & 24 deletions synapse/handlers/register.py
Original file line number Diff line number Diff line change
Expand Up @@ -636,6 +636,7 @@ async def post_registration_actions(self, user_id, auth_result, access_token):

if auth_result and LoginType.EMAIL_IDENTITY in auth_result:
threepid = auth_result[LoginType.EMAIL_IDENTITY]

# Necessary due to auth checks prior to the threepid being
# written to the db
if is_threepid_reserved(
Expand All @@ -645,34 +646,30 @@ async def post_registration_actions(self, user_id, auth_result, access_token):

await self.register_email_threepid(user_id, threepid, access_token)

if self.hs.config.account_threepid_delegate_email:
# Bind the 3PID to the identity server
if self.hs.config.bind_new_user_emails_to_sydent:
# Attempt to call Sydent's internal bind API on the given identity server
# to bind this threepid
id_server_url = self.hs.config.bind_new_user_emails_to_sydent

logger.debug(
"Binding email to %s on id_server %s",
"Attempting the bind email of %s to identity server: %s using "
"internal Sydent bind API.",
user_id,
self.hs.config.account_threepid_delegate_email,
self.hs.config.bind_new_user_emails_to_sydent,
)
threepid_creds = threepid["threepid_creds"]

# Remove the protocol scheme before handling to `bind_threepid`
# `bind_threepid` will add https:// to it, so this restricts
# account_threepid_delegate.email to https:// addresses only
# We assume this is always the case for dinsic however.
if self.hs.config.account_threepid_delegate_email.startswith(
"https://"
):
id_server = self.hs.config.account_threepid_delegate_email[8:]
else:
# Must start with http:// instead
id_server = self.hs.config.account_threepid_delegate_email[7:]

await self.identity_handler.bind_threepid(
threepid_creds["client_secret"],
threepid_creds["sid"],
user_id,
id_server,
threepid_creds.get("id_access_token"),
)
try:
await self.identity_handler.bind_email_using_internal_sydent_api(
id_server_url, threepid["address"], user_id
)
except Exception as e:
logger.warning(
"Failed to bind email of '%s' to Sydent instance '%s' ",
"using Sydent internal bind API: %s",
user_id,
id_server_url,
e,
)

if auth_result and LoginType.MSISDN in auth_result:
threepid = auth_result[LoginType.MSISDN]
Expand Down
85 changes: 84 additions & 1 deletion tests/handlers/test_register.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,16 @@
from synapse.api.constants import UserTypes
from synapse.api.errors import Codes, ResourceLimitError, SynapseError
from synapse.handlers.register import RegistrationHandler
from synapse.rest.client.v2_alpha.register import _map_email_to_displayname
from synapse.http.site import SynapseRequest
from synapse.rest.client.v2_alpha.register import (
_map_email_to_displayname,
register_servlets,
)
from synapse.types import RoomAlias, UserID, create_requester

from tests.server import FakeChannel
from tests.unittest import override_config

from .. import unittest


Expand All @@ -34,6 +41,10 @@ def __init__(self, hs):
class RegistrationTestCase(unittest.HomeserverTestCase):
""" Tests the RegistrationHandler. """

servlets = [
register_servlets,
]

def make_homeserver(self, reactor, clock):
hs_config = self.default_config()

Expand Down Expand Up @@ -287,6 +298,78 @@ def _check_mapping(self, i, expected):
result = _map_email_to_displayname(i)
self.assertEqual(result, expected)

@override_config(
{
"bind_new_user_emails_to_sydent": "https://is.example.com",
"registrations_require_3pid": ["email"],
"account_threepid_delegates": {},
"email": {
"smtp_host": "127.0.0.1",
"smtp_port": 20,
"require_transport_security": False,
"smtp_user": None,
"smtp_pass": None,
"notif_from": "test@example.com",
},
"public_baseurl": "http://localhost",
}
)
def test_user_email_bound_via_sydent_internal_api(self):
"""Tests that emails are bound after registration if this option is set"""
# Register user with an email address
email = "alice@example.com"

# Mock Synapse's threepid validator
get_threepid_validation_session = Mock(
return_value=defer.succeed(
{"medium": "email", "address": email, "validated_at": 0}
)
)
self.store.get_threepid_validation_session = get_threepid_validation_session
delete_threepid_session = Mock(return_value=defer.succeed(None))
self.store.delete_threepid_session = delete_threepid_session

# Mock Synapse's http json post method to check for the internal bind call
post_json_get_json = Mock(return_value=defer.succeed(None))
self.hs.get_simple_http_client().post_json_get_json = post_json_get_json

# Retrieve a UIA session ID
channel = self.uia_register(
401, {"username": "alice", "password": "nobodywillguessthis"}
)
session_id = channel.json_body["session"]

# Register our email address using the fake validation session above
channel = self.uia_register(
200,
{
"username": "alice",
"password": "nobodywillguessthis",
"auth": {
"session": session_id,
"type": "m.login.email.identity",
"threepid_creds": {"sid": "blabla", "client_secret": "blablabla"},
},
},
)
self.assertEqual(channel.json_body["user_id"], "@alice:test")

# Check that a bind attempt was made to our fake identity server
post_json_get_json.assert_called_with(
"https://is.example.com/_matrix/identity/internal/bind",
{"address": "alice@example.com", "medium": "email", "mxid": "@alice:test"},
)

def uia_register(self, expected_response: int, body: dict) -> FakeChannel:
"""Make a register request."""
request, channel = self.make_request(
"POST", "register", body
) # type: SynapseRequest, FakeChannel
self.render(request)

self.assertEqual(request.code, expected_response)
return channel

async def get_or_create_user(
self, requester, localpart, displayname, password_hash=None
):
Expand Down

0 comments on commit 21821c0

Please sign in to comment.