summary refs log tree commit diff
path: root/synapse/rest
diff options
context:
space:
mode:
Diffstat (limited to 'synapse/rest')
-rw-r--r--synapse/rest/client/v1/login.py159
1 files changed, 35 insertions, 124 deletions
diff --git a/synapse/rest/client/v1/login.py b/synapse/rest/client/v1/login.py

index 379f668d6f..3f116e5b44 100644 --- a/synapse/rest/client/v1/login.py +++ b/synapse/rest/client/v1/login.py
@@ -18,6 +18,7 @@ from typing import Awaitable, Callable, Dict, Optional from synapse.api.errors import Codes, LoginError, SynapseError from synapse.api.ratelimiting import Ratelimiter +from synapse.handlers.auth import client_dict_convert_legacy_fields_to_identifier from synapse.http.server import finish_request from synapse.http.servlet import ( RestServlet, @@ -28,56 +29,11 @@ from synapse.http.site import SynapseRequest from synapse.rest.client.v2_alpha._base import client_patterns from synapse.rest.well_known import WellKnownBuilder from synapse.types import JsonDict, UserID -from synapse.util.msisdn import phone_number_to_msisdn from synapse.util.threepids import canonicalise_email logger = logging.getLogger(__name__) -def login_submission_legacy_convert(submission): - """ - If the input login submission is an old style object - (ie. with top-level user / medium / address) convert it - to a typed object. - """ - if "user" in submission: - submission["identifier"] = {"type": "m.id.user", "user": submission["user"]} - del submission["user"] - - if "medium" in submission and "address" in submission: - submission["identifier"] = { - "type": "m.id.thirdparty", - "medium": submission["medium"], - "address": submission["address"], - } - del submission["medium"] - del submission["address"] - - -def login_id_thirdparty_from_phone(identifier): - """ - Convert a phone login identifier type to a generic threepid identifier - Args: - identifier(dict): Login identifier dict of type 'm.id.phone' - - Returns: Login identifier dict of type 'm.id.threepid' - """ - if "country" not in identifier or ( - # The specification requires a "phone" field, while Synapse used to require a "number" - # field. Accept both for backwards compatibility. - "phone" not in identifier - and "number" not in identifier - ): - raise SynapseError(400, "Invalid phone-type identifier") - - # Accept both "phone" and "number" as valid keys in m.id.phone - phone_number = identifier.get("phone", identifier["number"]) - - msisdn = phone_number_to_msisdn(identifier["country"], phone_number) - - return {"type": "m.id.thirdparty", "medium": "msisdn", "address": msisdn} - - class LoginRestServlet(RestServlet): PATTERNS = client_patterns("/login$", v1=True) CAS_TYPE = "m.login.cas" @@ -167,7 +123,8 @@ class LoginRestServlet(RestServlet): result = await self._do_token_login(login_submission) else: result = await self._do_other_login(login_submission) - except KeyError: + except KeyError as e: + logger.debug("KeyError during login: %s", e) raise SynapseError(400, "Missing JSON keys.") well_known_data = self._well_known_builder.get_well_known() @@ -194,27 +151,14 @@ class LoginRestServlet(RestServlet): 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") - + # Convert deprecated authdict formats to the current scheme + client_dict_convert_legacy_fields_to_identifier(login_submission) + + # Check whether this attempt uses a threepid, if so, check if our failed attempt + # ratelimiter allows another attempt at this time + medium = login_submission.get("medium") + address = login_submission.get("address") + if medium and address: # For emails, canonicalise the address. # We store all email addresses canonicalised in the DB. # (See add_threepid in synapse/handlers/auth.py) @@ -224,74 +168,41 @@ class LoginRestServlet(RestServlet): 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 + # Extract a localpart or user ID from the values in the identifier + username = await self.auth_handler.username_from_identifier( + login_submission["identifier"], login_submission.get("password") + ) - result = await self._complete_login( - canonical_user_id, login_submission, callback_3pid + if not username: + if medium and address: + # The user attempted to login via threepid and failed + # Record this failed attempt using the threepid as a key, as otherwise + # the user could bypass the ratelimiter by not providing a username + self._failed_attempts_ratelimiter.can_do_action( + (medium, address.lower()) ) - 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 - ) + raise LoginError(403, "Unauthorized threepid", errcode=Codes.FORBIDDEN) + + # The login failed for another reason + raise LoginError(403, "Invalid login", errcode=Codes.FORBIDDEN) + + # We were able to extract a username successfully + # Check if we've hit the failed ratelimit for this user ID + self._failed_attempts_ratelimiter.ratelimit(username.lower(), update=False) try: canonical_user_id, callback = await self.auth_handler.validate_login( - identifier["user"], login_submission + username, 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()) + # exception and masking the LoginError. This just records the attempt. + # The actual rate-limiting happens above + self._failed_attempts_ratelimiter.can_do_action(username.lower()) raise result = await self._complete_login( @@ -309,7 +220,7 @@ class LoginRestServlet(RestServlet): create_non_existent_users: bool = False, ) -> Dict[str, str]: """Called when we've successfully authed the user and now need to - actually login them in (e.g. create devices). This gets called on + actually log them in (e.g. create devices). This gets called on all successful logins. Applies the ratelimiting for successful login attempts against an