# # This file is licensed under the Affero General Public License (AGPL) version 3. # # Copyright 2021 The Matrix.org Foundation C.I.C. # Copyright 2014 - 2016 OpenMarket Ltd # Copyright (C) 2023 New Vector, Ltd # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as # published by the Free Software Foundation, either version 3 of the # License, or (at your option) any later version. # # See the GNU Affero General Public License for more details: # . # # Originally licensed under the Apache License, Version 2.0: # . # # [This file includes modifications made by New Vector Limited] # # """Contains functions for registering clients.""" import logging from typing import TYPE_CHECKING, Iterable, List, Optional, Tuple from prometheus_client import Counter from typing_extensions import TypedDict from synapse import types from synapse.api.constants import ( MAX_USERID_LENGTH, EventContentFields, EventTypes, JoinRules, LoginType, ) from synapse.api.errors import ( AuthError, Codes, ConsentNotGivenError, InvalidClientTokenError, SynapseError, ) from synapse.appservice import ApplicationService from synapse.config.server import is_threepid_reserved from synapse.handlers.device import DeviceHandler from synapse.http.servlet import assert_params_in_dict from synapse.replication.http.login import RegisterDeviceReplicationServlet from synapse.replication.http.register import ( ReplicationPostRegisterActionsServlet, ReplicationRegisterServlet, ) from synapse.spam_checker_api import RegistrationBehaviour from synapse.types import GUEST_USER_ID_PATTERN, RoomAlias, UserID, create_requester from synapse.types.state import StateFilter if TYPE_CHECKING: from synapse.server import HomeServer logger = logging.getLogger(__name__) registration_counter = Counter( "synapse_user_registrations_total", "Number of new users registered (since restart)", ["guest", "shadow_banned", "auth_provider"], ) login_counter = Counter( "synapse_user_logins_total", "Number of user logins (since restart)", ["guest", "auth_provider"], ) def init_counters_for_auth_provider(auth_provider_id: str) -> None: """Ensure the prometheus counters for the given auth provider are initialised This fixes a problem where the counters are not reported for a given auth provider until the user first logs in/registers. """ for is_guest in (True, False): login_counter.labels(guest=is_guest, auth_provider=auth_provider_id) for shadow_banned in (True, False): registration_counter.labels( guest=is_guest, shadow_banned=shadow_banned, auth_provider=auth_provider_id, ) class LoginDict(TypedDict): device_id: str access_token: str valid_until_ms: Optional[int] refresh_token: Optional[str] class RegistrationHandler: def __init__(self, hs: "HomeServer"): self.store = hs.get_datastores().main self._storage_controllers = hs.get_storage_controllers() self.clock = hs.get_clock() self.hs = hs self.auth = hs.get_auth() self.auth_blocking = hs.get_auth_blocking() self._auth_handler = hs.get_auth_handler() self.profile_handler = hs.get_profile_handler() self.user_directory_handler = hs.get_user_directory_handler() self.identity_handler = self.hs.get_identity_handler() self.ratelimiter = hs.get_registration_ratelimiter() self.macaroon_gen = hs.get_macaroon_generator() self._account_validity_handler = hs.get_account_validity_handler() self._user_consent_version = self.hs.config.consent.user_consent_version self._server_notices_mxid = hs.config.servernotices.server_notices_mxid self._server_name = hs.hostname self._spam_checker_module_callbacks = hs.get_module_api_callbacks().spam_checker if hs.config.worker.worker_app: self._register_client = ReplicationRegisterServlet.make_client(hs) self._register_device_client = RegisterDeviceReplicationServlet.make_client( hs ) self._post_registration_client = ( ReplicationPostRegisterActionsServlet.make_client(hs) ) else: self.device_handler = hs.get_device_handler() self._register_device_client = self.register_device_inner self.pusher_pool = hs.get_pusherpool() self.session_lifetime = hs.config.registration.session_lifetime self.nonrefreshable_access_token_lifetime = ( hs.config.registration.nonrefreshable_access_token_lifetime ) self.refreshable_access_token_lifetime = ( hs.config.registration.refreshable_access_token_lifetime ) self.refresh_token_lifetime = hs.config.registration.refresh_token_lifetime init_counters_for_auth_provider("") async def check_username( self, localpart: str, guest_access_token: Optional[str] = None, assigned_user_id: Optional[str] = None, allow_invalid: bool = False, inhibit_user_in_use_error: bool = False, ) -> None: # meow: allow admins to register invalid user ids if not allow_invalid: if types.contains_invalid_mxid_characters(localpart): raise SynapseError( 400, "User ID can only contain characters a-z, 0-9, or '=_-./+'", Codes.INVALID_USERNAME, ) if not localpart: raise SynapseError(400, "User ID cannot be empty", Codes.INVALID_USERNAME) if localpart[0] == "_": raise SynapseError( 400, "User ID may not begin with _", Codes.INVALID_USERNAME ) user = UserID(localpart, self.hs.hostname) user_id = user.to_string() if assigned_user_id: if user_id == assigned_user_id: return else: raise SynapseError( 400, "A different user ID has already been registered for this session", ) # meow: allow admins to register reserved user ids and long user ids if not allow_invalid: self.check_user_id_not_appservice_exclusive(user_id) if len(user_id) > MAX_USERID_LENGTH: raise SynapseError( 400, "User ID may not be longer than %s characters" % (MAX_USERID_LENGTH,), Codes.INVALID_USERNAME, ) users = await self.store.get_users_by_id_case_insensitive(user_id) if users: if not inhibit_user_in_use_error and not guest_access_token: raise SynapseError( 400, "User ID already taken.", errcode=Codes.USER_IN_USE ) if guest_access_token: user_data = await self.auth.get_user_by_access_token(guest_access_token) if not user_data.is_guest or user_data.user.localpart != localpart: raise AuthError( 403, "Cannot register taken user ID without valid guest " "credentials for that user.", errcode=Codes.FORBIDDEN, ) if guest_access_token is None and GUEST_USER_ID_PATTERN.fullmatch(localpart): raise SynapseError( 400, "Numeric user IDs are reserved for guest users.", errcode=Codes.INVALID_USERNAME, ) async def register_user( self, localpart: Optional[str] = None, password_hash: Optional[str] = None, guest_access_token: Optional[str] = None, make_guest: bool = False, admin: bool = False, threepid: Optional[dict] = None, user_type: Optional[str] = None, default_display_name: Optional[str] = None, address: Optional[str] = None, bind_emails: Optional[Iterable[str]] = None, by_admin: bool = False, user_agent_ips: Optional[List[Tuple[str, str]]] = None, auth_provider_id: Optional[str] = None, approved: bool = False, ) -> str: """Registers a new client on the server. Args: localpart: The local part of the user ID to register. If None, one will be generated. password_hash: The hashed password to assign to this user so they can login again. This can be None which means they cannot login again via a password (e.g. the user is an application service user). guest_access_token: The access token used when this was a guest account. make_guest: True if the the new user should be guest, false to add a regular user account. admin: True if the user should be registered as a server admin. threepid: The threepid used for registering, if any. user_type: type of user. One of the values from api.constants.UserTypes, or None for a normal user. default_display_name: if set, the new user's displayname will be set to this. Defaults to 'localpart'. address: the IP address used to perform the registration. bind_emails: list of emails to bind to this account. by_admin: True if this registration is being made via the admin api, otherwise False. user_agent_ips: Tuples of user-agents and IP addresses used during the registration process. auth_provider_id: The SSO IdP the user used, if any. approved: True if the new user should be considered already approved by an administrator. Returns: The registered user_id. Raises: SynapseError if there was a problem registering. """ bind_emails = bind_emails or [] await self.check_registration_ratelimit(address) result = await self._spam_checker_module_callbacks.check_registration_for_spam( threepid, localpart, user_agent_ips or [], auth_provider_id=auth_provider_id, ) if result == RegistrationBehaviour.DENY: logger.info( "Blocked registration of %r", localpart, ) # We return a 429 to make it not obvious that they've been # denied. raise SynapseError(429, "Rate limited") shadow_banned = result == RegistrationBehaviour.SHADOW_BAN if shadow_banned: logger.info( "Shadow banning registration of %r", localpart, ) # do not check_auth_blocking if the call is coming through the Admin API if not by_admin: await self.auth_blocking.check_auth_blocking(threepid=threepid) if localpart is not None: allow_invalid = by_admin and self.hs.config.meow.admin_api_register_invalid await self.check_username( localpart, guest_access_token=guest_access_token, allow_invalid=allow_invalid, ) was_guest = guest_access_token is not None user = UserID(localpart, self.hs.hostname) user_id = user.to_string() if was_guest: # If the user was a guest then they already have a profile default_display_name = None elif default_display_name is None: default_display_name = localpart await self.register_with_store( user_id=user_id, password_hash=password_hash, was_guest=was_guest, make_guest=make_guest, create_profile_with_displayname=default_display_name, admin=admin, user_type=user_type, address=address, shadow_banned=shadow_banned, approved=approved, ) profile = await self.store.get_profileinfo(user) await self.user_directory_handler.handle_local_profile_change( user_id, profile ) else: # autogen a sequential user ID fail_count = 0 # If a default display name is not given, generate one. generate_display_name = default_display_name is None # This breaks on successful registration *or* errors after 10 failures. while True: # Fail after being unable to find a suitable ID a few times if fail_count > 10: raise SynapseError(500, "Unable to find a suitable guest user ID") generated_localpart = await self.store.generate_user_id() user = UserID(generated_localpart, self.hs.hostname) user_id = user.to_string() self.check_user_id_not_appservice_exclusive(user_id) if generate_display_name: default_display_name = generated_localpart try: await self.register_with_store( user_id=user_id, password_hash=password_hash, make_guest=make_guest, create_profile_with_displayname=default_display_name, address=address, shadow_banned=shadow_banned, ) # Successfully registered break except SynapseError: # if user id is taken, just generate another fail_count += 1 registration_counter.labels( guest=make_guest, shadow_banned=shadow_banned, auth_provider=(auth_provider_id or ""), ).inc() # If the user does not need to consent at registration, auto-join any # configured rooms. if not self.hs.config.consent.user_consent_at_registration: if ( not self.hs.config.registration.auto_join_rooms_for_guests and make_guest ): logger.info( "Skipping auto-join for %s because auto-join for guests is disabled", user_id, ) else: await self._auto_join_rooms(user_id) else: logger.info( "Skipping auto-join for %s because consent is required at registration", user_id, ) # Bind any specified emails to this account current_time = self.hs.get_clock().time_msec() for email in bind_emails: # generate threepid dict threepid_dict = { "medium": "email", "address": email, "validated_at": current_time, } # Bind email to new account await self._register_email_threepid(user_id, threepid_dict, None) return user_id async def _create_and_join_rooms(self, user_id: str) -> None: """ Create the auto-join rooms and join or invite the user to them. This should only be called when the first "real" user registers. Args: user_id: The user to join """ # Getting the handlers during init gives a dependency loop. room_creation_handler = self.hs.get_room_creation_handler() room_member_handler = self.hs.get_room_member_handler() # Generate a stub for how the rooms will be configured. stub_config = { "preset": self.hs.config.registration.autocreate_auto_join_room_preset, } # If the configuration provides a user ID to create rooms with, use # that instead of the first user registered. requires_join = False if self.hs.config.registration.auto_join_user_id: fake_requester = create_requester( self.hs.config.registration.auto_join_user_id, authenticated_entity=self._server_name, ) # If the room requires an invite, add the user to the list of invites. if self.hs.config.registration.auto_join_room_requires_invite: stub_config["invite"] = [user_id] # If the room is being created by a different user, the first user # registered needs to join it. Note that in the case of an invitation # being necessary this will occur after the invite was sent. requires_join = True else: fake_requester = create_requester( user_id, authenticated_entity=self._server_name ) # Choose whether to federate the new room. if not self.hs.config.registration.autocreate_auto_join_rooms_federated: stub_config["creation_content"] = {EventContentFields.FEDERATE: False} for r in self.hs.config.registration.auto_join_rooms: logger.info("Auto-joining %s to %s", user_id, r) try: room_alias = RoomAlias.from_string(r) if self.hs.hostname != room_alias.domain: # If the alias is remote, try to join the room. This might fail # because the room might be invite only, but we don't have any local # user in the room to invite this one with, so at this point that's # the best we can do. logger.info( "Cannot automatically create room with alias %s as it isn't" " local, trying to join the room instead", r, ) ( room, remote_room_hosts, ) = await room_member_handler.lookup_room_alias(room_alias) room_id = room.to_string() await room_member_handler.update_membership( requester=create_requester( user_id, authenticated_entity=self._server_name ), target=UserID.from_string(user_id), room_id=room_id, remote_room_hosts=remote_room_hosts, action="join", ratelimit=False, ) else: # A shallow copy is OK here since the only key that is # modified is room_alias_name. config = stub_config.copy() # create room expects the localpart of the room alias config["room_alias_name"] = room_alias.localpart room_id, _, _ = await room_creation_handler.create_room( fake_requester, config=config, ratelimit=False, ) # If the room does not require an invite, but another user # created it, then ensure the first user joins it. if requires_join: await room_member_handler.update_membership( requester=create_requester( user_id, authenticated_entity=self._server_name ), target=UserID.from_string(user_id), room_id=room_id, # Since it was just created, there are no remote hosts. remote_room_hosts=[], action="join", ratelimit=False, ) except Exception as e: logger.error("Failed to join new user to %r: %r", r, e) async def _join_rooms(self, user_id: str) -> None: """ Join or invite the user to the auto-join rooms. Args: user_id: The user to join """ room_member_handler = self.hs.get_room_member_handler() for r in self.hs.config.registration.auto_join_rooms: logger.info("Auto-joining %s to %s", user_id, r) try: room_alias = RoomAlias.from_string(r) if RoomAlias.is_valid(r): ( room, remote_room_hosts, ) = await room_member_handler.lookup_room_alias(room_alias) room_id = room.to_string() else: raise SynapseError( 400, "%s was not legal room ID or room alias" % (r,) ) # Calculate whether the room requires an invite or can be # joined directly. By default, we consider the room as requiring an # invite if the homeserver is in the room (unless told otherwise by the # join rules). Otherwise we consider it as being joinable, at the risk of # failing to join, but in this case there's little more we can do since # we don't have a local user in the room to craft up an invite with. requires_invite = await self.store.is_host_joined( room_id, self._server_name, ) if requires_invite: # If the server is in the room, check if the room is public. state = await self._storage_controllers.state.get_current_state_ids( room_id, StateFilter.from_types([(EventTypes.JoinRules, "")]) ) event_id = state.get((EventTypes.JoinRules, "")) if event_id: join_rules_event = await self.store.get_event( event_id, allow_none=True ) if join_rules_event: join_rule = join_rules_event.content.get("join_rule", None) requires_invite = ( join_rule and join_rule != JoinRules.PUBLIC ) # Send the invite, if necessary. if requires_invite: # If an invite is required, there must be a auto-join user ID. assert self.hs.config.registration.auto_join_user_id await room_member_handler.update_membership( requester=create_requester( self.hs.config.registration.auto_join_user_id, authenticated_entity=self._server_name, ), target=UserID.from_string(user_id), room_id=room_id, remote_room_hosts=remote_room_hosts, action="invite", ratelimit=False, ) # Send the join. await room_member_handler.update_membership( requester=create_requester( user_id, authenticated_entity=self._server_name ), target=UserID.from_string(user_id), room_id=room_id, remote_room_hosts=remote_room_hosts, action="join", ratelimit=False, ) except ConsentNotGivenError as e: # Technically not necessary to pull out this error though # moving away from bare excepts is a good thing to do. logger.error("Failed to join new user to %r: %r", r, e) except Exception as e: logger.error("Failed to join new user to %r: %r", r, e, exc_info=True) async def _auto_join_rooms(self, user_id: str) -> None: """Automatically joins users to auto join rooms - creating the room in the first place if the user is the first to be created. Args: user_id: The user to join """ # If there are no rooms to auto-join, just bail. if not self.hs.config.registration.auto_join_rooms: return # auto-join the user to any rooms we're supposed to dump them into # try to create the room if we're the first real user on the server. Note # that an auto-generated support or bot user is not a real user and will never be # the user to create the room should_auto_create_rooms = False if ( self.hs.config.registration.autocreate_auto_join_rooms and await self.store.is_real_user(user_id) ): count = await self.store.count_real_users() should_auto_create_rooms = count == 1 if should_auto_create_rooms: await self._create_and_join_rooms(user_id) else: await self._join_rooms(user_id) async def post_consent_actions(self, user_id: str) -> None: """A series of registration actions that can only be carried out once consent has been granted Args: user_id: The user to join """ await self._auto_join_rooms(user_id) async def appservice_register(self, user_localpart: str, as_token: str) -> str: user = UserID(user_localpart, self.hs.hostname) user_id = user.to_string() service = self.store.get_app_service_by_token(as_token) if not service: raise InvalidClientTokenError() if not service.is_interested_in_user(user_id): raise SynapseError( 400, "Invalid user localpart for this application service.", errcode=Codes.EXCLUSIVE, ) service_id = service.id if service.is_exclusive_user(user_id) else None self.check_user_id_not_appservice_exclusive(user_id, allowed_appservice=service) await self.register_with_store( user_id=user_id, password_hash="", appservice_id=service_id, create_profile_with_displayname=user.localpart, ) return user_id def check_user_id_not_appservice_exclusive( self, user_id: str, allowed_appservice: Optional[ApplicationService] = None ) -> None: # don't allow people to register the server notices mxid if self._server_notices_mxid is not None: if user_id == self._server_notices_mxid: raise SynapseError( 400, "This user ID is reserved.", errcode=Codes.EXCLUSIVE ) # valid user IDs must not clash with any user ID namespaces claimed by # application services. services = self.store.get_app_services() interested_services = [ s for s in services if s.is_interested_in_user(user_id) and s != allowed_appservice ] for service in interested_services: if service.is_exclusive_user(user_id): raise SynapseError( 400, "This user ID is reserved by an application service.", errcode=Codes.EXCLUSIVE, ) async def check_registration_ratelimit(self, address: Optional[str]) -> None: """A simple helper method to check whether the registration rate limit has been hit for a given IP address Args: address: the IP address used to perform the registration. If this is None, no ratelimiting will be performed. Raises: LimitExceededError: If the rate limit has been exceeded. """ if not address: return await self.ratelimiter.ratelimit(None, address) async def register_with_store( self, user_id: str, password_hash: Optional[str] = None, was_guest: bool = False, make_guest: bool = False, appservice_id: Optional[str] = None, create_profile_with_displayname: Optional[str] = None, admin: bool = False, user_type: Optional[str] = None, address: Optional[str] = None, shadow_banned: bool = False, approved: bool = False, ) -> None: """Register user in the datastore. Args: user_id: The desired user ID to register. password_hash: Optional. The password hash for this user. was_guest: Optional. Whether this is a guest account being upgraded to a non-guest account. make_guest: True if the the new user should be guest, false to add a regular user account. appservice_id: The ID of the appservice registering the user. create_profile_with_displayname: Optionally create a profile for the user, setting their displayname to the given value admin: is an admin user? user_type: type of user. One of the values from api.constants.UserTypes, or None for a normal user. address: the IP address used to perform the registration. shadow_banned: Whether to shadow-ban the user approved: Whether to mark the user as approved by an administrator """ if self.hs.config.worker.worker_app: await self._register_client( user_id=user_id, password_hash=password_hash, was_guest=was_guest, make_guest=make_guest, appservice_id=appservice_id, create_profile_with_displayname=create_profile_with_displayname, admin=admin, user_type=user_type, address=address, shadow_banned=shadow_banned, approved=approved, ) else: await self.store.register_user( user_id=user_id, password_hash=password_hash, was_guest=was_guest, make_guest=make_guest, appservice_id=appservice_id, create_profile_with_displayname=create_profile_with_displayname, admin=admin, user_type=user_type, shadow_banned=shadow_banned, approved=approved, ) # Only call the account validity module(s) on the main process, to avoid # repeating e.g. database writes on all of the workers. await self._account_validity_handler.on_user_registration(user_id) async def register_device( self, user_id: str, device_id: Optional[str], initial_display_name: Optional[str], is_guest: bool = False, is_appservice_ghost: bool = False, auth_provider_id: Optional[str] = None, should_issue_refresh_token: bool = False, auth_provider_session_id: Optional[str] = None, ) -> Tuple[str, str, Optional[int], Optional[str]]: """Register a device for a user and generate an access token. The access token will be limited by the homeserver's session_lifetime config. Args: user_id: full canonical @user:id device_id: The device ID to check, or None to generate a new one. initial_display_name: An optional display name for the device. is_guest: Whether this is a guest account auth_provider_id: The SSO IdP the user used, if any. should_issue_refresh_token: Whether it should also issue a refresh token auth_provider_session_id: The session ID received during login from the SSO IdP. Returns: Tuple of device ID, access token, access token expiration time and refresh token """ res = await self._register_device_client( user_id=user_id, device_id=device_id, initial_display_name=initial_display_name, is_guest=is_guest, is_appservice_ghost=is_appservice_ghost, should_issue_refresh_token=should_issue_refresh_token, auth_provider_id=auth_provider_id, auth_provider_session_id=auth_provider_session_id, ) login_counter.labels( guest=is_guest, auth_provider=(auth_provider_id or ""), ).inc() return ( res["device_id"], res["access_token"], res["valid_until_ms"], res["refresh_token"], ) async def register_device_inner( self, user_id: str, device_id: Optional[str], initial_display_name: Optional[str], is_guest: bool = False, is_appservice_ghost: bool = False, should_issue_refresh_token: bool = False, auth_provider_id: Optional[str] = None, auth_provider_session_id: Optional[str] = None, ) -> LoginDict: """Helper for register_device Does the bits that need doing on the main process. Not for use outside this class and RegisterDeviceReplicationServlet. """ assert not self.hs.config.worker.worker_app now_ms = self.clock.time_msec() access_token_expiry = None if self.session_lifetime is not None: if is_guest: raise Exception( "session_lifetime is not currently implemented for guest access" ) access_token_expiry = now_ms + self.session_lifetime if self.nonrefreshable_access_token_lifetime is not None: if access_token_expiry is not None: # Don't allow the non-refreshable access token to outlive the # session. access_token_expiry = min( now_ms + self.nonrefreshable_access_token_lifetime, access_token_expiry, ) else: access_token_expiry = now_ms + self.nonrefreshable_access_token_lifetime refresh_token = None refresh_token_id = None # This can only run on the main process. assert isinstance(self.device_handler, DeviceHandler) registered_device_id = await self.device_handler.check_device_registered( user_id, device_id, initial_display_name, auth_provider_id=auth_provider_id, auth_provider_session_id=auth_provider_session_id, ) if is_guest: assert access_token_expiry is None access_token = self.macaroon_gen.generate_guest_access_token(user_id) else: if should_issue_refresh_token: # A refreshable access token lifetime must be configured # since we're told to issue a refresh token (the caller checks # that this value is set before setting this flag). assert self.refreshable_access_token_lifetime is not None # Set the expiry time of the refreshable access token access_token_expiry = now_ms + self.refreshable_access_token_lifetime # Set the refresh token expiry time (if configured) refresh_token_expiry = None if self.refresh_token_lifetime is not None: refresh_token_expiry = now_ms + self.refresh_token_lifetime # Set an ultimate session expiry time (if configured) ultimate_session_expiry_ts = None if self.session_lifetime is not None: ultimate_session_expiry_ts = now_ms + self.session_lifetime # Also ensure that the issued tokens don't outlive the # session. # (It would be weird to configure a homeserver with a shorter # session lifetime than token lifetime, but may as well handle # it.) access_token_expiry = min( access_token_expiry, ultimate_session_expiry_ts ) if refresh_token_expiry is not None: refresh_token_expiry = min( refresh_token_expiry, ultimate_session_expiry_ts ) ( refresh_token, refresh_token_id, ) = await self._auth_handler.create_refresh_token_for_user_id( user_id, device_id=registered_device_id, expiry_ts=refresh_token_expiry, ultimate_session_expiry_ts=ultimate_session_expiry_ts, ) access_token = await self._auth_handler.create_access_token_for_user_id( user_id, device_id=registered_device_id, valid_until_ms=access_token_expiry, is_appservice_ghost=is_appservice_ghost, refresh_token_id=refresh_token_id, ) return { "device_id": registered_device_id, "access_token": access_token, "valid_until_ms": access_token_expiry, "refresh_token": refresh_token, } async def post_registration_actions( self, user_id: str, auth_result: dict, access_token: Optional[str] ) -> None: """A user has completed registration Args: user_id: The user ID that consented auth_result: The authenticated credentials of the newly registered user. access_token: The access token of the newly logged in device, or None if `inhibit_login` enabled. """ # TODO: 3pid registration can actually happen on the workers. Consider # refactoring it. if self.hs.config.worker.worker_app: await self._post_registration_client( user_id=user_id, auth_result=auth_result, access_token=access_token ) return 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( self.hs.config.server.mau_limits_reserved_threepids, threepid ): await self.store.upsert_monthly_active_user(user_id) await self._register_email_threepid(user_id, threepid, access_token) if auth_result and LoginType.MSISDN in auth_result: threepid = auth_result[LoginType.MSISDN] await self._register_msisdn_threepid(user_id, threepid) if auth_result and LoginType.TERMS in auth_result: # The terms type should only exist if consent is enabled. assert self._user_consent_version is not None await self._on_user_consented(user_id, self._user_consent_version) async def _on_user_consented(self, user_id: str, consent_version: str) -> None: """A user consented to the terms on registration Args: user_id: The user ID that consented. consent_version: version of the policy the user has consented to. """ logger.info("%s has consented to the privacy policy", user_id) await self.store.user_set_consent_version(user_id, consent_version) await self.post_consent_actions(user_id) async def _register_email_threepid( self, user_id: str, threepid: dict, token: Optional[str] ) -> None: """Add an email address as a 3pid identifier Also adds an email pusher for the email address, if configured in the HS config Must be called on master. Args: user_id: id of user threepid: m.login.email.identity auth response token: access_token for the user, or None if not logged in. """ reqd = ("medium", "address", "validated_at") if any(x not in threepid for x in reqd): # This will only happen if the ID server returns a malformed response logger.info("Can't add incomplete 3pid") return await self._auth_handler.add_threepid( user_id, threepid["medium"], threepid["address"], threepid["validated_at"], ) # And we add an email pusher for them by default, but only # if email notifications are enabled (so people don't start # getting mail spam where they weren't before if email # notifs are set up on a homeserver) if ( self.hs.config.email.email_enable_notifs and self.hs.config.email.email_notif_for_new_users and token ): # Pull the ID of the access token back out of the db # It would really make more sense for this to be passed # up when the access token is saved, but that's quite an # invasive change I'd rather do separately. user_tuple = await self.store.get_user_by_access_token(token) # The token better still exist. assert user_tuple device_id = user_tuple.device_id await self.pusher_pool.add_or_update_pusher( user_id=user_id, device_id=device_id, kind="email", app_id="m.email", app_display_name="Email Notifications", device_display_name=threepid["address"], pushkey=threepid["address"], lang=None, data={}, ) async def _register_msisdn_threepid(self, user_id: str, threepid: dict) -> None: """Add a phone number as a 3pid identifier Must be called on master. Args: user_id: id of user threepid: m.login.msisdn auth response """ try: assert_params_in_dict(threepid, ["medium", "address", "validated_at"]) except SynapseError as ex: if ex.errcode == Codes.MISSING_PARAM: # This will only happen if the ID server returns a malformed response logger.info("Can't add incomplete 3pid") return None raise await self._auth_handler.add_threepid( user_id, threepid["medium"], threepid["address"], threepid["validated_at"], )