summary refs log tree commit diff
path: root/synapse/handlers
diff options
context:
space:
mode:
Diffstat (limited to 'synapse/handlers')
-rw-r--r--synapse/handlers/auth.py216
1 files changed, 204 insertions, 12 deletions
diff --git a/synapse/handlers/auth.py b/synapse/handlers/auth.py

index 654f58ddae..2d64ee5e44 100644 --- a/synapse/handlers/auth.py +++ b/synapse/handlers/auth.py
@@ -38,12 +38,14 @@ from synapse.api.ratelimiting import Ratelimiter from synapse.handlers.ui_auth import INTERACTIVE_AUTH_CHECKERS from synapse.handlers.ui_auth.checkers import UserInteractiveAuthChecker from synapse.http.server import finish_request, respond_with_html +from synapse.http.servlet import assert_params_in_dict from synapse.http.site import SynapseRequest from synapse.logging.context import defer_to_thread from synapse.metrics.background_process_metrics import run_as_background_process from synapse.module_api import ModuleApi from synapse.types import Requester, UserID from synapse.util import stringutils as stringutils +from synapse.util.msisdn import phone_number_to_msisdn from synapse.util.threepids import canonicalise_email from ._base import BaseHandler @@ -51,6 +53,82 @@ from ._base import BaseHandler logger = logging.getLogger(__name__) +def client_dict_convert_legacy_fields_to_identifier( + submission: Dict[str, Union[str, Dict]] +): + """ + Convert a legacy-formatted login submission to an identifier dict. + + Legacy login submissions (used in both login and user-interactive authentication) + provide user-identifying information at the top-level instead of in an `indentifier` + property. This is now deprecated and replaced with identifiers: + https://matrix.org/docs/spec/client_server/r0.6.1#identifier-types + + Args: + submission: The client dict to convert. Passed by reference and modified + + Raises: + SynapseError: If the format of the client dict is invalid + """ + if "user" in submission: + submission["identifier"] = {"type": "m.id.user", "user": submission.pop("user")} + + if "medium" in submission and "address" in submission: + submission["identifier"] = { + "type": "m.id.thirdparty", + "medium": submission.pop("medium"), + "address": submission.pop("address"), + } + + # We've converted valid, legacy login submissions to an identifier. If the + # dict still doesn't have an identifier, it's invalid + assert_params_in_dict(submission, required=["identifier"]) + + # Ensure the identifier has a type + if "type" not in submission["identifier"]: + raise SynapseError( + 400, "'identifier' dict has no key 'type'", errcode=Codes.MISSING_PARAM, + ) + + +def login_id_phone_to_thirdparty(identifier: Dict[str, str]) -> Dict[str, str]: + """Convert a phone login identifier type to a generic threepid identifier. + + Args: + identifier: Login identifier dict of type 'm.id.phone' + + Returns: + An equivalent m.id.thirdparty identifier dict. + """ + if "type" not in identifier: + raise SynapseError( + 400, "Invalid phone-type identifier", errcode=Codes.MISSING_PARAM + ) + + if "country" not in identifier or ( + # XXX: We used to require `number` instead of `phone`. The spec + # defines `phone`. So accept both + "phone" not in identifier + and "number" not in identifier + ): + raise SynapseError( + 400, "Invalid phone-type identifier", errcode=Codes.INVALID_PARAM + ) + + # Accept both "phone" and "number" as valid keys in m.id.phone + phone_number = identifier.get("phone", identifier.get("number")) + + # Convert user-provided phone number to a consistent representation + msisdn = phone_number_to_msisdn(identifier["country"], phone_number) + + # Return the new dictionary + return { + "type": "m.id.thirdparty", + "medium": "msisdn", + "address": msisdn, + } + + class AuthHandler(BaseHandler): SESSION_EXPIRE_MS = 48 * 60 * 60 * 1000 @@ -319,7 +397,7 @@ class AuthHandler(BaseHandler): # otherwise use whatever was last provided. # # This was designed to allow the client to omit the parameters - # and just supply the session in subsequent calls so it split + # and just supply the session in subsequent calls. So it splits # auth between devices by just sharing the session, (eg. so you # could continue registration from your phone having clicked the # email auth link on there). It's probably too open to abuse @@ -524,16 +602,129 @@ class AuthHandler(BaseHandler): res = await checker.check_auth(authdict, clientip=clientip) return res - # build a v1-login-style dict out of the authdict and fall back to the - # v1 code - user_id = authdict.get("user") + # We don't have a checker for the auth type provided by the client + # Assume that it is `m.login.password`. + if login_type != LoginType.PASSWORD: + raise SynapseError( + 400, "Unknown authentication type", errcode=Codes.INVALID_PARAM, + ) + + password = authdict.get("password") + if password is None: + raise SynapseError( + 400, + "Missing parameter for m.login.password dict: 'password'", + errcode=Codes.INVALID_PARAM, + ) + + # Retrieve the user ID using details provided in the authdict + + # Deprecation notice: Clients used to be able to simply provide a + # `user` field which pointed to a user_id or localpart. This has + # been deprecated in favour of an `identifier` key, which is a + # dictionary providing information on how to identify a single + # user. + # https://matrix.org/docs/spec/client_server/r0.6.1#identifier-types + # + # We convert old-style dicts to new ones here + client_dict_convert_legacy_fields_to_identifier(authdict) + + # Extract a user ID from the values in the identifier + username = await self.username_from_identifier(authdict["identifier"], password) - if user_id is None: - raise SynapseError(400, "", Codes.MISSING_PARAM) + if username is None: + raise SynapseError(400, "Valid username not found") + + # Now that we've found the username, validate that the password is correct + canonical_id, _ = await self.validate_login(username, authdict) - (canonical_id, callback) = await self.validate_login(user_id, authdict) return canonical_id + async def username_from_identifier( + self, identifier: Dict[str, str], password: Optional[str] = None + ) -> Optional[str]: + """Given a dictionary containing an identifier from a client, extract the + possibly unqualified username of the user that it identifies. Does *not* + guarantee that the user exists. + + If this identifier dict contains a threepid, we attempt to ask password + auth providers about it or, failing that, look up an associated user in + the database. + + Args: + identifier: The identifier dictionary provided by the client + password: The user provided password if one exists. Used for asking + password auth providers for usernames from 3pid+password combos. + + Returns: + A username if one was found, or None otherwise + + Raises: + SynapseError: If the identifier dict is invalid + """ + + # Convert phone type identifiers to generic threepid identifiers, which + # will be handled in the next step + if identifier["type"] == "m.id.phone": + identifier = login_id_phone_to_thirdparty(identifier) + + # Convert a threepid identifier to an user identifier + if identifier["type"] == "m.id.thirdparty": + address = identifier.get("address") + medium = identifier.get("medium") + + if not medium or not address: + # An error would've already been raised in + # `login_id_thirdparty_from_phone` if the original submission + # was a phone identifier + raise SynapseError( + 400, "Invalid thirdparty identifier", errcode=Codes.INVALID_PARAM, + ) + + if medium == "email": + # For emails, transform the address to lowercase. + # We store all email addresses as lowercase in the DB. + # (See add_threepid in synapse/handlers/auth.py) + address = address.lower() + + # Check for auth providers that support 3pid login types + if password is not None: + canonical_user_id, _ = await self.check_password_provider_3pid( + medium, address, password, + ) + if canonical_user_id: + # Authentication through password provider and 3pid succeeded + return canonical_user_id + + # Check local store + user_id = await self.hs.get_datastore().get_user_id_by_threepid( + medium, address + ) + if not user_id: + # We were unable to find a user_id that belonged to the threepid returned + # by the password auth provider + return None + + identifier = {"type": "m.id.user", "user": user_id} + + # By this point, the identifier should be a `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", errcode=Codes.INVALID_PARAM, + ) + + # User identifiers have a "user" key + user = identifier.get("user") + if user is None: + raise SynapseError( + 400, + "User identifier is missing 'user' key", + errcode=Codes.INVALID_PARAM, + ) + + return user + def _get_params_recaptcha(self) -> dict: return {"public_key": self.hs.config.recaptcha_public_key} @@ -698,7 +889,8 @@ class AuthHandler(BaseHandler): m.login.password auth types. Args: - username: username supplied by the user + username: a localpart or fully qualified user ID - what is provided by the + client login_submission: the whole of the login submission (including 'type' and other relevant fields) Returns: @@ -710,10 +902,10 @@ class AuthHandler(BaseHandler): LoginError if there was an authentication problem. """ - if username.startswith("@"): - qualified_user_id = username - else: - qualified_user_id = UserID(username, self.hs.hostname).to_string() + # We need a fully qualified User ID for some method calls here + qualified_user_id = username + if not qualified_user_id.startswith("@"): + qualified_user_id = UserID(qualified_user_id, self.hs.hostname).to_string() login_type = login_submission.get("type") known_login_type = False