async def add_threepid(self, user_id: str, medium: str, address: str, validated_at: int): # check if medium has a valid value if medium not in ["email", "msisdn"]: raise SynapseError( code=400, msg=("'%s' is not a valid value for 'medium'" % (medium, )), errcode=Codes.INVALID_PARAM, ) # 'Canonicalise' email addresses down to lower case. # We've now moving towards the homeserver being the entity that # is responsible for validating threepids used for resetting passwords # on accounts, so in future Synapse will gain knowledge of specific # types (mediums) of threepid. For now, we still use the existing # infrastructure, but this is the start of synapse gaining knowledge # of specific types of threepid (and fixes the fact that checking # for the presence of an email address during password reset was # case sensitive). if medium == "email": address = canonicalise_email(address) await self.store.user_add_threepid(user_id, medium, address, validated_at, self.hs.get_clock().time_msec())
async def delete_threepid( self, user_id: str, medium: str, address: str, id_server: Optional[str] = None ) -> bool: """Attempts to unbind the 3pid on the identity servers and deletes it from the local database. Args: user_id: ID of user to remove the 3pid from. medium: The medium of the 3pid being removed: "email" or "msisdn". address: The 3pid address to remove. id_server: Use the given identity server when unbinding any threepids. If None then will attempt to unbind using the identity server specified when binding (if known). Returns: Returns True if successfully unbound the 3pid on the identity server, False if identity server doesn't support the unbind API. """ # 'Canonicalise' email addresses as per above if medium == "email": address = canonicalise_email(address) identity_handler = self.hs.get_handlers().identity_handler result = await identity_handler.try_unbind_threepid( user_id, {"medium": medium, "address": address, "id_server": id_server} ) await self.store.user_delete_threepid(user_id, medium, address) return result
async def get_registered_reserved_users(self) -> List[str]: """Of the reserved threepids defined in config, retrieve those that are associated with registered users Returns: User IDs of actual users that are reserved """ users = [] for tp in self.hs.config.server.mau_limits_reserved_threepids[ : self.hs.config.server.max_mau_value ]: user_id = await self.hs.get_datastore().get_user_id_by_threepid( tp["medium"], canonicalise_email(tp["address"]) ) if user_id: users.append(user_id) return users
def test_address_trim(self): self.assertEqual(canonicalise_email(" [email protected] "), "*****@*****.**")
def test_address_casefold(self): self.assertEqual(canonicalise_email("Strauß@Example.com"), "*****@*****.**")
def test_domain_with_umlaut(self): self.assertEqual(canonicalise_email("foo@Öumlaut.com"), "foo@öumlaut.com")
def test_domain_to_lower(self): self.assertEqual(canonicalise_email("*****@*****.**"), "*****@*****.**")
def test_valid_format(self): self.assertEqual(canonicalise_email("*****@*****.**"), "*****@*****.**")
async def on_POST(self, request): if self.config.threepid_behaviour_email == ThreepidBehaviour.OFF: if self.config.local_threepid_handling_disabled_due_to_email_config: logger.warning( "User password resets have been disabled due to lack of email config" ) raise SynapseError( 400, "Email-based password resets have been disabled on this server" ) body = parse_json_object_from_request(request) assert_params_in_dict(body, ["client_secret", "email", "send_attempt"]) # Extract params from body client_secret = body["client_secret"] assert_valid_client_secret(client_secret) # Canonicalise the email address. The addresses are all stored canonicalised # in the database. This allows the user to reset his password without having to # know the exact spelling (eg. upper and lower case) of address in the database. # Stored in the database "*****@*****.**" # User requests with "*****@*****.**" would raise a Not Found error try: email = canonicalise_email(body["email"]) except ValueError as e: raise SynapseError(400, str(e)) send_attempt = body["send_attempt"] next_link = body.get("next_link") # Optional param if not check_3pid_allowed(self.hs, "email", email): raise SynapseError( 403, "Your email domain is not authorized on this server", Codes.THREEPID_DENIED, ) # The email will be sent to the stored address. # This avoids a potential account hijack by requesting a password reset to # an email address which is controlled by the attacker but which, after # canonicalisation, matches the one in our database. existing_user_id = await self.hs.get_datastore( ).get_user_id_by_threepid("email", email) if existing_user_id is None: if self.config.request_token_inhibit_3pid_errors: # Make the client think the operation succeeded. See the rationale in the # comments for request_token_inhibit_3pid_errors. return 200, {"sid": random_string(16)} raise SynapseError(400, "Email not found", Codes.THREEPID_NOT_FOUND) if self.config.threepid_behaviour_email == ThreepidBehaviour.REMOTE: assert self.hs.config.account_threepid_delegate_email # Have the configured identity server handle the request ret = await self.identity_handler.requestEmailToken( self.hs.config.account_threepid_delegate_email, email, client_secret, send_attempt, next_link, ) else: # Send password reset emails from Synapse sid = await self.identity_handler.send_threepid_validation( email, client_secret, send_attempt, self.mailer.send_password_reset_mail, next_link, ) # Wrap the session id in a JSON object ret = {"sid": sid} return 200, ret
def test_no_at(self): with self.assertRaises(ValueError): canonicalise_email("address-without-at.bar")
async def _do_other_login(self, login_submission: JsonDict) -> Dict[str, str]: """Handle non-token/saml/jwt logins Args: login_submission: Returns: HTTP response """ # Log the request we got, but only certain fields to minimise the chance of # logging someone's password (even if they accidentally put it in the wrong # field) logger.info( "Got login request with identifier: %r, medium: %r, address: %r, user: %r", login_submission.get("identifier"), login_submission.get("medium"), login_submission.get("address"), login_submission.get("user"), ) login_submission_legacy_convert(login_submission) if "identifier" not in login_submission: raise SynapseError(400, "Missing param: identifier") identifier = login_submission["identifier"] if "type" not in identifier: raise SynapseError(400, "Login identifier has no type") # convert phone type identifiers to generic threepids if identifier["type"] == "m.id.phone": identifier = login_id_thirdparty_from_phone(identifier) # convert threepid identifiers to user IDs if identifier["type"] == "m.id.thirdparty": address = identifier.get("address") medium = identifier.get("medium") if medium is None or address is None: raise SynapseError(400, "Invalid thirdparty identifier") # For emails, canonicalise the address. # We store all email addresses canonicalised in the DB. # (See add_threepid in synapse/handlers/auth.py) if medium == "email": try: address = canonicalise_email(address) except ValueError as e: raise SynapseError(400, str(e)) # We also apply account rate limiting using the 3PID as a key, as # otherwise using 3PID bypasses the ratelimiting based on user ID. self._failed_attempts_ratelimiter.ratelimit((medium, address), update=False) # Check for login providers that support 3pid login types ( canonical_user_id, callback_3pid, ) = await self.auth_handler.check_password_provider_3pid( medium, address, login_submission["password"] ) if canonical_user_id: # Authentication through password provider and 3pid succeeded result = await self._complete_login( canonical_user_id, login_submission, callback_3pid ) return result # No password providers were able to handle this 3pid # Check local store user_id = await self.hs.get_datastore().get_user_id_by_threepid( medium, address ) if not user_id: logger.warning( "unknown 3pid identifier medium %s, address %r", medium, address ) # We mark that we've failed to log in here, as # `check_password_provider_3pid` might have returned `None` due # to an incorrect password, rather than the account not # existing. # # If it returned None but the 3PID was bound then we won't hit # this code path, which is fine as then the per-user ratelimit # will kick in below. self._failed_attempts_ratelimiter.can_do_action((medium, address)) raise LoginError(403, "", errcode=Codes.FORBIDDEN) identifier = {"type": "m.id.user", "user": user_id} # by this point, the identifier should be an m.id.user: if it's anything # else, we haven't understood it. if identifier["type"] != "m.id.user": raise SynapseError(400, "Unknown login identifier type") if "user" not in identifier: raise SynapseError(400, "User identifier is missing 'user' key") if identifier["user"].startswith("@"): qualified_user_id = identifier["user"] else: qualified_user_id = UserID(identifier["user"], self.hs.hostname).to_string() # Check if we've hit the failed ratelimit (but don't update it) self._failed_attempts_ratelimiter.ratelimit( qualified_user_id.lower(), update=False ) try: canonical_user_id, callback = await self.auth_handler.validate_login( identifier["user"], login_submission ) except LoginError: # The user has failed to log in, so we need to update the rate # limiter. Using `can_do_action` avoids us raising a ratelimit # exception and masking the LoginError. The actual ratelimiting # should have happened above. self._failed_attempts_ratelimiter.can_do_action(qualified_user_id.lower()) raise result = await self._complete_login( canonical_user_id, login_submission, callback ) return result
async def on_POST(self, request): body = parse_json_object_from_request(request) # we do basic sanity checks here because the auth layer will store these # in sessions. Pull out the new password provided to us. new_password = body.pop("new_password", None) if new_password is not None: if not isinstance(new_password, str) or len(new_password) > 512: raise SynapseError(400, "Invalid password") self.password_policy_handler.validate_password(new_password) # there are two possibilities here. Either the user does not have an # access token, and needs to do a password reset; or they have one and # need to validate their identity. # # In the first case, we offer a couple of means of identifying # themselves (email and msisdn, though it's unclear if msisdn actually # works). # # In the second case, we require a password to confirm their identity. if self.auth.has_access_token(request): requester = await self.auth.get_user_by_req(request) try: params, session_id = await self.auth_handler.validate_user_via_ui_auth( requester, request, body, self.hs.get_ip_from_request(request), "modify your account password", ) except InteractiveAuthIncompleteError as e: # The user needs to provide more steps to complete auth, but # they're not required to provide the password again. # # If a password is available now, hash the provided password and # store it for later. if new_password: password_hash = await self.auth_handler.hash(new_password) await self.auth_handler.set_session_data( e.session_id, "password_hash", password_hash) raise user_id = requester.user.to_string() else: requester = None try: result, params, session_id = await self.auth_handler.check_ui_auth( [[LoginType.EMAIL_IDENTITY]], request, body, self.hs.get_ip_from_request(request), "modify your account password", ) except InteractiveAuthIncompleteError as e: # The user needs to provide more steps to complete auth, but # they're not required to provide the password again. # # If a password is available now, hash the provided password and # store it for later. if new_password: password_hash = await self.auth_handler.hash(new_password) await self.auth_handler.set_session_data( e.session_id, "password_hash", password_hash) raise if LoginType.EMAIL_IDENTITY in result: threepid = result[LoginType.EMAIL_IDENTITY] if "medium" not in threepid or "address" not in threepid: raise SynapseError(500, "Malformed threepid") if threepid["medium"] == "email": # For emails, canonicalise the address. # We store all email addresses canonicalised in the DB. # (See add_threepid in synapse/handlers/auth.py) try: threepid["address"] = canonicalise_email( threepid["address"]) except ValueError as e: raise SynapseError(400, str(e)) # if using email, we must know about the email they're authing with! threepid_user_id = await self.datastore.get_user_id_by_threepid( threepid["medium"], threepid["address"]) if not threepid_user_id: raise SynapseError(404, "Email address not found", Codes.NOT_FOUND) user_id = threepid_user_id else: logger.error("Auth succeeded but no known type! %r", result.keys()) raise SynapseError(500, "", Codes.UNKNOWN) # If we have a password in this request, prefer it. Otherwise, there # must be a password hash from an earlier request. if new_password: password_hash = await self.auth_handler.hash(new_password) else: password_hash = await self.auth_handler.get_session_data( session_id, "password_hash", None) if not password_hash: raise SynapseError(400, "Missing params: password", Codes.MISSING_PARAM) logout_devices = params.get("logout_devices", True) await self._set_password_handler.set_password(user_id, password_hash, logout_devices, requester) return 200, {}
async def on_POST(self, request): if self.hs.config.threepid_behaviour_email == ThreepidBehaviour.OFF: if self.hs.config.local_threepid_handling_disabled_due_to_email_config: logger.warning( "Email registration has been disabled due to lack of email config" ) raise SynapseError( 400, "Email-based registration has been disabled on this server" ) body = parse_json_object_from_request(request) assert_params_in_dict(body, ["client_secret", "email", "send_attempt"]) # Extract params from body client_secret = body["client_secret"] assert_valid_client_secret(client_secret) # For emails, canonicalise the address. # We store all email addresses canonicalised in the DB. # (See on_POST in EmailThreepidRequestTokenRestServlet # in synapse/rest/client/v2_alpha/account.py) try: email = canonicalise_email(body["email"]) except ValueError as e: raise SynapseError(400, str(e)) send_attempt = body["send_attempt"] next_link = body.get("next_link") # Optional param if not check_3pid_allowed(self.hs, "email", email): raise SynapseError( 403, "Your email domain is not authorized to register on this server", Codes.THREEPID_DENIED, ) existing_user_id = await self.hs.get_datastore().get_user_id_by_threepid( "email", email ) if existing_user_id is not None: if self.hs.config.request_token_inhibit_3pid_errors: # Make the client think the operation succeeded. See the rationale in the # comments for request_token_inhibit_3pid_errors. # Also wait for some random amount of time between 100ms and 1s to make it # look like we did something. await self.hs.get_clock().sleep(random.randint(1, 10) / 10) return 200, {"sid": random_string(16)} raise SynapseError(400, "Email is already in use", Codes.THREEPID_IN_USE) if self.config.threepid_behaviour_email == ThreepidBehaviour.REMOTE: assert self.hs.config.account_threepid_delegate_email # Have the configured identity server handle the request ret = await self.identity_handler.requestEmailToken( self.hs.config.account_threepid_delegate_email, email, client_secret, send_attempt, next_link, ) else: # Send registration emails from Synapse sid = await self.identity_handler.send_threepid_validation( email, client_secret, send_attempt, self.mailer.send_registration_mail, next_link, ) # Wrap the session id in a JSON object ret = {"sid": sid} threepid_send_requests.labels(type="email", reason="register").observe( send_attempt ) return 200, ret
async def add_pusher( self, user_id: str, access_token: Optional[int], kind: str, app_id: str, app_display_name: str, device_display_name: str, pushkey: str, lang: Optional[str], data: JsonDict, profile_tag: str = "", ) -> Optional[Pusher]: """Creates a new pusher and adds it to the pool Returns: The newly created pusher. """ if kind == "email": email_owner = await self.store.get_user_id_by_threepid( "email", canonicalise_email(pushkey) ) if email_owner != user_id: raise SynapseError(400, "Email not found", Codes.THREEPID_NOT_FOUND) time_now_msec = self.clock.time_msec() # create the pusher setting last_stream_ordering to the current maximum # stream ordering, so it will process pushes from this point onwards. last_stream_ordering = self.store.get_room_max_stream_ordering() # we try to create the pusher just to validate the config: it # will then get pulled out of the database, # recreated, added and started: this means we have only one # code path adding pushers. self.pusher_factory.create_pusher( PusherConfig( id=None, user_name=user_id, access_token=access_token, profile_tag=profile_tag, kind=kind, app_id=app_id, app_display_name=app_display_name, device_display_name=device_display_name, pushkey=pushkey, ts=time_now_msec, lang=lang, data=data, last_stream_ordering=last_stream_ordering, last_success=None, failing_since=None, ) ) await self.store.add_pusher( user_id=user_id, access_token=access_token, kind=kind, app_id=app_id, app_display_name=app_display_name, device_display_name=device_display_name, pushkey=pushkey, pushkey_ts=time_now_msec, lang=lang, data=data, last_stream_ordering=last_stream_ordering, profile_tag=profile_tag, ) pusher = await self.start_pusher_by_id(app_id, pushkey, user_id) return pusher
async def on_POST(self, request): body = parse_json_object_from_request(request) client_addr = request.getClientIP() self.ratelimiter.ratelimit(client_addr, update=False) kind = b"user" if b"kind" in request.args: kind = request.args[b"kind"][0] if kind == b"guest": ret = await self._do_guest_registration(body, address=client_addr) return ret elif kind != b"user": raise UnrecognizedRequestError( "Do not understand membership kind: %s" % (kind.decode("utf8"), )) # we do basic sanity checks here because the auth layer will store these # in sessions. Pull out the username/password provided to us. if "password" in body: password = body.pop("password") if not isinstance(password, str) or len(password) > 512: raise SynapseError(400, "Invalid password") self.password_policy_handler.validate_password(password) # If the password is valid, hash it and store it back on the body. # This ensures that only the hashed password is handled everywhere. if "password_hash" in body: raise SynapseError(400, "Unexpected property: password_hash") body["password_hash"] = await self.auth_handler.hash(password) desired_username = None if "username" in body: if not isinstance(body["username"], str) or len(body["username"]) > 512: raise SynapseError(400, "Invalid username") desired_username = body["username"] appservice = None if self.auth.has_access_token(request): appservice = await self.auth.get_appservice_by_req(request) # fork off as soon as possible for ASes which have completely # different registration flows to normal users # == Application Service Registration == if appservice: # Set the desired user according to the AS API (which uses the # 'user' key not 'username'). Since this is a new addition, we'll # fallback to 'username' if they gave one. desired_username = body.get("user", desired_username) # XXX we should check that desired_username is valid. Currently # we give appservices carte blanche for any insanity in mxids, # because the IRC bridges rely on being able to register stupid # IDs. access_token = self.auth.get_access_token_from_request(request) if isinstance(desired_username, str): result = await self._do_appservice_registration( desired_username, access_token, body) return 200, result # we throw for non 200 responses # for regular registration, downcase the provided username before # attempting to register it. This should mean # that people who try to register with upper-case in their usernames # don't get a nasty surprise. (Note that we treat username # case-insenstively in login, so they are free to carry on imagining # that their username is CrAzYh4cKeR if that keeps them happy) if desired_username is not None: desired_username = desired_username.lower() # == Normal User Registration == (everyone else) if not self.hs.config.enable_registration: raise SynapseError(403, "Registration has been disabled") guest_access_token = body.get("guest_access_token", None) if "initial_device_display_name" in body and "password_hash" not in body: # ignore 'initial_device_display_name' if sent without # a password to work around a client bug where it sent # the 'initial_device_display_name' param alone, wiping out # the original registration params logger.warning( "Ignoring initial_device_display_name without password") del body["initial_device_display_name"] session_id = self.auth_handler.get_session_id(body) registered_user_id = None if session_id: # if we get a registered user id out of here, it means we previously # registered a user for this session, so we could just return the # user here. We carry on and go through the auth checks though, # for paranoia. registered_user_id = await self.auth_handler.get_session_data( session_id, "registered_user_id", None) if desired_username is not None: await self.registration_handler.check_username( desired_username, guest_access_token=guest_access_token, assigned_user_id=registered_user_id, ) auth_result, params, session_id = await self.auth_handler.check_auth( self._registration_flows, request, body, self.hs.get_ip_from_request(request), "register a new account", ) # Check that we're not trying to register a denied 3pid. # # the user-facing checks will probably already have happened in # /register/email/requestToken when we requested a 3pid, but that's not # guaranteed. if auth_result: for login_type in [LoginType.EMAIL_IDENTITY, LoginType.MSISDN]: if login_type in auth_result: medium = auth_result[login_type]["medium"] address = auth_result[login_type]["address"] if not check_3pid_allowed(self.hs, medium, address): raise SynapseError( 403, "Third party identifiers (email/phone numbers)" + " are not authorized on this server", Codes.THREEPID_DENIED, ) if registered_user_id is not None: logger.info("Already registered user ID %r for this session", registered_user_id) # don't re-register the threepids registered = False else: # NB: This may be from the auth handler and NOT from the POST assert_params_in_dict(params, ["password_hash"]) desired_username = params.get("username", None) guest_access_token = params.get("guest_access_token", None) new_password_hash = params.get("password_hash", None) if desired_username is not None: desired_username = desired_username.lower() threepid = None if auth_result: threepid = auth_result.get(LoginType.EMAIL_IDENTITY) # Also check that we're not trying to register a 3pid that's already # been registered. # # This has probably happened in /register/email/requestToken as well, # but if a user hits this endpoint twice then clicks on each link from # the two activation emails, they would register the same 3pid twice. for login_type in [LoginType.EMAIL_IDENTITY, LoginType.MSISDN]: if login_type in auth_result: medium = auth_result[login_type]["medium"] address = auth_result[login_type]["address"] # For emails, canonicalise the address. # We store all email addresses canonicalised in the DB. # (See on_POST in EmailThreepidRequestTokenRestServlet # in synapse/rest/client/v2_alpha/account.py) if medium == "email": try: address = canonicalise_email(address) except ValueError as e: raise SynapseError(400, str(e)) existing_user_id = await self.store.get_user_id_by_threepid( medium, address) if existing_user_id is not None: raise SynapseError( 400, "%s is already in use" % medium, Codes.THREEPID_IN_USE, ) registered_user_id = await self.registration_handler.register_user( localpart=desired_username, password_hash=new_password_hash, guest_access_token=guest_access_token, threepid=threepid, address=client_addr, ) # Necessary due to auth checks prior to the threepid being # written to the db if threepid: if is_threepid_reserved( self.hs.config.mau_limits_reserved_threepids, threepid): await self.store.upsert_monthly_active_user( registered_user_id) # remember that we've now registered that user account, and with # what user ID (since the user may not have specified) await self.auth_handler.set_session_data(session_id, "registered_user_id", registered_user_id) registered = True return_dict = await self._create_registration_details( registered_user_id, params) if registered: await self.registration_handler.post_registration_actions( user_id=registered_user_id, auth_result=auth_result, access_token=return_dict.get("access_token"), ) return 200, return_dict
async def on_POST(self, request): body = parse_json_object_from_request(request) # we do basic sanity checks here because the auth layer will store these # in sessions. Pull out the new password provided to us. if "new_password" in body: new_password = body.pop("new_password") if not isinstance(new_password, str) or len(new_password) > 512: raise SynapseError(400, "Invalid password") self.password_policy_handler.validate_password(new_password) # If the password is valid, hash it and store it back on the body. # This ensures that only the hashed password is handled everywhere. if "new_password_hash" in body: raise SynapseError(400, "Unexpected property: new_password_hash") body["new_password_hash"] = await self.auth_handler.hash( new_password) # there are two possibilities here. Either the user does not have an # access token, and needs to do a password reset; or they have one and # need to validate their identity. # # In the first case, we offer a couple of means of identifying # themselves (email and msisdn, though it's unclear if msisdn actually # works). # # In the second case, we require a password to confirm their identity. if self.auth.has_access_token(request): requester = await self.auth.get_user_by_req(request) params = await self.auth_handler.validate_user_via_ui_auth( requester, request, body, self.hs.get_ip_from_request(request), "modify your account password", ) user_id = requester.user.to_string() else: requester = None result, params, _ = await self.auth_handler.check_auth( [[LoginType.EMAIL_IDENTITY]], request, body, self.hs.get_ip_from_request(request), "modify your account password", ) if LoginType.EMAIL_IDENTITY in result: threepid = result[LoginType.EMAIL_IDENTITY] if "medium" not in threepid or "address" not in threepid: raise SynapseError(500, "Malformed threepid") if threepid["medium"] == "email": # For emails, canonicalise the address. # We store all email addresses canonicalised in the DB. # (See add_threepid in synapse/handlers/auth.py) try: threepid["address"] = canonicalise_email( threepid["address"]) except ValueError as e: raise SynapseError(400, str(e)) # if using email, we must know about the email they're authing with! threepid_user_id = await self.datastore.get_user_id_by_threepid( threepid["medium"], threepid["address"]) if not threepid_user_id: raise SynapseError(404, "Email address not found", Codes.NOT_FOUND) user_id = threepid_user_id else: logger.error("Auth succeeded but no known type! %r", result.keys()) raise SynapseError(500, "", Codes.UNKNOWN) assert_params_in_dict(params, ["new_password_hash"]) new_password_hash = params["new_password_hash"] logout_devices = params.get("logout_devices", True) await self._set_password_handler.set_password(user_id, new_password_hash, logout_devices, requester) return 200, {}
async def on_POST(self, request): if self.config.threepid_behaviour_email == ThreepidBehaviour.OFF: if self.config.local_threepid_handling_disabled_due_to_email_config: logger.warning( "Adding emails have been disabled due to lack of an email config" ) raise SynapseError( 400, "Adding an email to your account is disabled on this server") body = parse_json_object_from_request(request) assert_params_in_dict(body, ["client_secret", "email", "send_attempt"]) client_secret = body["client_secret"] assert_valid_client_secret(client_secret) # Canonicalise the email address. The addresses are all stored canonicalised # in the database. # This ensures that the validation email is sent to the canonicalised address # as it will later be entered into the database. # Otherwise the email will be sent to "*****@*****.**" and stored as # "*****@*****.**" in database. try: email = canonicalise_email(body["email"]) except ValueError as e: raise SynapseError(400, str(e)) send_attempt = body["send_attempt"] next_link = body.get("next_link") # Optional param if not check_3pid_allowed(self.hs, "email", email): raise SynapseError( 403, "Your email domain is not authorized on this server", Codes.THREEPID_DENIED, ) existing_user_id = await self.store.get_user_id_by_threepid( "email", email) if existing_user_id is not None: if self.config.request_token_inhibit_3pid_errors: # Make the client think the operation succeeded. See the rationale in the # comments for request_token_inhibit_3pid_errors. return 200, {"sid": random_string(16)} raise SynapseError(400, "Email is already in use", Codes.THREEPID_IN_USE) if self.config.threepid_behaviour_email == ThreepidBehaviour.REMOTE: assert self.hs.config.account_threepid_delegate_email # Have the configured identity server handle the request ret = await self.identity_handler.requestEmailToken( self.hs.config.account_threepid_delegate_email, email, client_secret, send_attempt, next_link, ) else: # Send threepid validation emails from Synapse sid = await self.identity_handler.send_threepid_validation( email, client_secret, send_attempt, self.mailer.send_add_threepid_mail, next_link, ) # Wrap the session id in a JSON object ret = {"sid": sid} return 200, ret
def test_two_at(self): with self.assertRaises(ValueError): canonicalise_email("foo@[email protected]")
async def on_POST(self, request: SynapseRequest) -> Tuple[int, JsonDict]: body = parse_json_object_from_request(request) client_addr = request.getClientIP() await self.ratelimiter.ratelimit(None, client_addr, update=False) kind = parse_string(request, "kind", default="user") if kind == "guest": ret = await self._do_guest_registration(body, address=client_addr) return ret elif kind != "user": raise UnrecognizedRequestError( f"Do not understand membership kind: {kind}", ) # Check if the clients wishes for this registration to issue a refresh # token. client_requested_refresh_tokens = body.get("refresh_token", False) if not isinstance(client_requested_refresh_tokens, bool): raise SynapseError(400, "`refresh_token` should be true or false.") should_issue_refresh_token = (self._refresh_tokens_enabled and client_requested_refresh_tokens) # Pull out the provided username and do basic sanity checks early since # the auth layer will store these in sessions. desired_username = None if "username" in body: if not isinstance(body["username"], str) or len(body["username"]) > 512: raise SynapseError(400, "Invalid username") desired_username = body["username"] # fork off as soon as possible for ASes which have completely # different registration flows to normal users # == Application Service Registration == if body.get("type") == APP_SERVICE_REGISTRATION_TYPE: if not self.auth.has_access_token(request): raise SynapseError( 400, "Appservice token must be provided when using a type of m.login.application_service", ) # Verify the AS self.auth.get_appservice_by_req(request) # Set the desired user according to the AS API (which uses the # 'user' key not 'username'). Since this is a new addition, we'll # fallback to 'username' if they gave one. desired_username = body.get("user", desired_username) # XXX we should check that desired_username is valid. Currently # we give appservices carte blanche for any insanity in mxids, # because the IRC bridges rely on being able to register stupid # IDs. access_token = self.auth.get_access_token_from_request(request) if not isinstance(desired_username, str): raise SynapseError( 400, "Desired Username is missing or not a string") result = await self._do_appservice_registration( desired_username, access_token, body, should_issue_refresh_token=should_issue_refresh_token, ) return 200, result elif self.auth.has_access_token(request): raise SynapseError( 400, "An access token should not be provided on requests to /register (except if type is m.login.application_service)", ) # == Normal User Registration == (everyone else) if not self._registration_enabled: raise SynapseError(403, "Registration has been disabled", Codes.FORBIDDEN) # For regular registration, convert the provided username to lowercase # before attempting to register it. This should mean that people who try # to register with upper-case in their usernames don't get a nasty surprise. # # Note that we treat usernames case-insensitively in login, so they are # free to carry on imagining that their username is CrAzYh4cKeR if that # keeps them happy. if desired_username is not None: desired_username = desired_username.lower() # Check if this account is upgrading from a guest account. guest_access_token = body.get("guest_access_token", None) # Pull out the provided password and do basic sanity checks early. # # Note that we remove the password from the body since the auth layer # will store the body in the session and we don't want a plaintext # password store there. password = body.pop("password", None) if password is not None: if not isinstance(password, str) or len(password) > 512: raise SynapseError(400, "Invalid password") self.password_policy_handler.validate_password(password) if "initial_device_display_name" in body and password is None: # ignore 'initial_device_display_name' if sent without # a password to work around a client bug where it sent # the 'initial_device_display_name' param alone, wiping out # the original registration params logger.warning( "Ignoring initial_device_display_name without password") del body["initial_device_display_name"] session_id = self.auth_handler.get_session_id(body) registered_user_id = None password_hash = None if session_id: # if we get a registered user id out of here, it means we previously # registered a user for this session, so we could just return the # user here. We carry on and go through the auth checks though, # for paranoia. registered_user_id = await self.auth_handler.get_session_data( session_id, UIAuthSessionDataConstants.REGISTERED_USER_ID, None) # Extract the previously-hashed password from the session. password_hash = await self.auth_handler.get_session_data( session_id, UIAuthSessionDataConstants.PASSWORD_HASH, None) # Ensure that the username is valid. if desired_username is not None: await self.registration_handler.check_username( desired_username, guest_access_token=guest_access_token, assigned_user_id=registered_user_id, ) # Check if the user-interactive authentication flows are complete, if # not this will raise a user-interactive auth error. try: auth_result, params, session_id = await self.auth_handler.check_ui_auth( self._registration_flows, request, body, "register a new account", ) except InteractiveAuthIncompleteError as e: # The user needs to provide more steps to complete auth. # # Hash the password and store it with the session since the client # is not required to provide the password again. # # If a password hash was previously stored we will not attempt to # re-hash and store it for efficiency. This assumes the password # does not change throughout the authentication flow, but this # should be fine since the data is meant to be consistent. if not password_hash and password: password_hash = await self.auth_handler.hash(password) await self.auth_handler.set_session_data( e.session_id, UIAuthSessionDataConstants.PASSWORD_HASH, password_hash, ) raise # Check that we're not trying to register a denied 3pid. # # the user-facing checks will probably already have happened in # /register/email/requestToken when we requested a 3pid, but that's not # guaranteed. if auth_result: for login_type in [LoginType.EMAIL_IDENTITY, LoginType.MSISDN]: if login_type in auth_result: medium = auth_result[login_type]["medium"] address = auth_result[login_type]["address"] if not check_3pid_allowed(self.hs, medium, address): raise SynapseError( 403, "Third party identifiers (email/phone numbers)" + " are not authorized on this server", Codes.THREEPID_DENIED, ) if registered_user_id is not None: logger.info("Already registered user ID %r for this session", registered_user_id) # don't re-register the threepids registered = False else: # If we have a password in this request, prefer it. Otherwise, there # might be a password hash from an earlier request. if password: password_hash = await self.auth_handler.hash(password) if not password_hash: raise SynapseError(400, "Missing params: password", Codes.MISSING_PARAM) desired_username = params.get("username", None) guest_access_token = params.get("guest_access_token", None) if desired_username is not None: desired_username = desired_username.lower() threepid = None if auth_result: threepid = auth_result.get(LoginType.EMAIL_IDENTITY) # Also check that we're not trying to register a 3pid that's already # been registered. # # This has probably happened in /register/email/requestToken as well, # but if a user hits this endpoint twice then clicks on each link from # the two activation emails, they would register the same 3pid twice. for login_type in [LoginType.EMAIL_IDENTITY, LoginType.MSISDN]: if login_type in auth_result: medium = auth_result[login_type]["medium"] address = auth_result[login_type]["address"] # For emails, canonicalise the address. # We store all email addresses canonicalised in the DB. # (See on_POST in EmailThreepidRequestTokenRestServlet # in synapse/rest/client/account.py) if medium == "email": try: address = canonicalise_email(address) except ValueError as e: raise SynapseError(400, str(e)) existing_user_id = await self.store.get_user_id_by_threepid( medium, address) if existing_user_id is not None: raise SynapseError( 400, "%s is already in use" % medium, Codes.THREEPID_IN_USE, ) entries = await self.store.get_user_agents_ips_to_ui_auth_session( session_id) registered_user_id = await self.registration_handler.register_user( localpart=desired_username, password_hash=password_hash, guest_access_token=guest_access_token, threepid=threepid, address=client_addr, user_agent_ips=entries, ) # Necessary due to auth checks prior to the threepid being # written to the db if threepid: if is_threepid_reserved( self.hs.config.server.mau_limits_reserved_threepids, threepid): await self.store.upsert_monthly_active_user( registered_user_id) # Remember that the user account has been registered (and the user # ID it was registered with, since it might not have been specified). await self.auth_handler.set_session_data( session_id, UIAuthSessionDataConstants.REGISTERED_USER_ID, registered_user_id, ) registered = True return_dict = await self._create_registration_details( registered_user_id, params, should_issue_refresh_token=should_issue_refresh_token, ) if registered: # Check if a token was used to authenticate registration registration_token = await self.auth_handler.get_session_data( session_id, UIAuthSessionDataConstants.REGISTRATION_TOKEN, ) if registration_token: # Increment the `completed` counter for the token await self.store.use_registration_token(registration_token) # Indicate that the token has been successfully used so that # pending is not decremented again when expiring old UIA sessions. await self.store.mark_ui_auth_stage_complete( session_id, LoginType.REGISTRATION_TOKEN, True, ) await self.registration_handler.post_registration_actions( user_id=registered_user_id, auth_result=auth_result, access_token=return_dict.get("access_token"), ) return 200, return_dict
def test_bad_format(self): with self.assertRaises(ValueError): canonicalise_email("[email protected]@good.example.com")