Ejemplo n.º 1
0
class LoginRestServlet(RestServlet):
    PATTERNS = client_patterns("/login$", v1=True)
    CAS_TYPE = "m.login.cas"
    SSO_TYPE = "m.login.sso"
    TOKEN_TYPE = "m.login.token"
    JWT_TYPE = "org.matrix.login.jwt"
    JWT_TYPE_DEPRECATED = "m.login.jwt"
    APPSERVICE_TYPE = "uk.half-shot.msc2778.login.application_service"

    def __init__(self, hs: "HomeServer"):
        super().__init__()
        self.hs = hs

        # JWT configuration variables.
        self.jwt_enabled = hs.config.jwt_enabled
        self.jwt_secret = hs.config.jwt_secret
        self.jwt_algorithm = hs.config.jwt_algorithm
        self.jwt_issuer = hs.config.jwt_issuer
        self.jwt_audiences = hs.config.jwt_audiences

        # SSO configuration.
        self.saml2_enabled = hs.config.saml2_enabled
        self.cas_enabled = hs.config.cas_enabled
        self.oidc_enabled = hs.config.oidc_enabled

        self.auth = hs.get_auth()

        self.auth_handler = self.hs.get_auth_handler()
        self.registration_handler = hs.get_registration_handler()
        self._well_known_builder = WellKnownBuilder(hs)
        self._address_ratelimiter = Ratelimiter(
            clock=hs.get_clock(),
            rate_hz=self.hs.config.rc_login_address.per_second,
            burst_count=self.hs.config.rc_login_address.burst_count,
        )
        self._account_ratelimiter = Ratelimiter(
            clock=hs.get_clock(),
            rate_hz=self.hs.config.rc_login_account.per_second,
            burst_count=self.hs.config.rc_login_account.burst_count,
        )

    def on_GET(self, request: SynapseRequest):
        flows = []
        if self.jwt_enabled:
            flows.append({"type": LoginRestServlet.JWT_TYPE})
            flows.append({"type": LoginRestServlet.JWT_TYPE_DEPRECATED})

        if self.cas_enabled:
            # we advertise CAS for backwards compat, though MSC1721 renamed it
            # to SSO.
            flows.append({"type": LoginRestServlet.CAS_TYPE})

        if self.cas_enabled or self.saml2_enabled or self.oidc_enabled:
            flows.append({"type": LoginRestServlet.SSO_TYPE})
            # While its valid for us to advertise this login type generally,
            # synapse currently only gives out these tokens as part of the
            # SSO login flow.
            # Generally we don't want to advertise login flows that clients
            # don't know how to implement, since they (currently) will always
            # fall back to the fallback API if they don't understand one of the
            # login flow types returned.
            flows.append({"type": LoginRestServlet.TOKEN_TYPE})

        flows.extend(({
            "type": t
        } for t in self.auth_handler.get_supported_login_types()))

        flows.append({"type": LoginRestServlet.APPSERVICE_TYPE})

        return 200, {"flows": flows}

    async def on_POST(self, request: SynapseRequest):
        login_submission = parse_json_object_from_request(request)

        try:
            if login_submission["type"] == LoginRestServlet.APPSERVICE_TYPE:
                appservice = self.auth.get_appservice_by_req(request)

                if appservice.is_rate_limited():
                    self._address_ratelimiter.ratelimit(request.getClientIP())

                result = await self._do_appservice_login(
                    login_submission, appservice)
            elif self.jwt_enabled and (
                    login_submission["type"] == LoginRestServlet.JWT_TYPE
                    or login_submission["type"]
                    == LoginRestServlet.JWT_TYPE_DEPRECATED):
                self._address_ratelimiter.ratelimit(request.getClientIP())
                result = await self._do_jwt_login(login_submission)
            elif login_submission["type"] == LoginRestServlet.TOKEN_TYPE:
                self._address_ratelimiter.ratelimit(request.getClientIP())
                result = await self._do_token_login(login_submission)
            else:
                self._address_ratelimiter.ratelimit(request.getClientIP())
                result = await self._do_other_login(login_submission)
        except KeyError:
            raise SynapseError(400, "Missing JSON keys.")

        well_known_data = self._well_known_builder.get_well_known()
        if well_known_data:
            result["well_known"] = well_known_data
        return 200, result

    async def _do_appservice_login(self, login_submission: JsonDict,
                                   appservice: ApplicationService):
        identifier = login_submission.get("identifier")
        logger.info("Got appservice login request with identifier: %r",
                    identifier)

        if not isinstance(identifier, dict):
            raise SynapseError(400, "Invalid identifier in login submission",
                               Codes.INVALID_PARAM)

        # this login flow only supports identifiers of type "m.id.user".
        if identifier.get("type") != "m.id.user":
            raise SynapseError(400, "Unknown login identifier type",
                               Codes.INVALID_PARAM)

        user = identifier.get("user")
        if not isinstance(user, str):
            raise SynapseError(400, "Invalid user in identifier",
                               Codes.INVALID_PARAM)

        if user.startswith("@"):
            qualified_user_id = user
        else:
            qualified_user_id = UserID(user, self.hs.hostname).to_string()

        if not appservice.is_interested_in_user(qualified_user_id):
            raise LoginError(403,
                             "Invalid access_token",
                             errcode=Codes.FORBIDDEN)

        return await self._complete_login(
            qualified_user_id,
            login_submission,
            ratelimit=appservice.is_rate_limited())

    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"),
        )
        canonical_user_id, callback = await self.auth_handler.validate_login(
            login_submission, ratelimit=True)
        result = await self._complete_login(canonical_user_id,
                                            login_submission, callback)
        return result

    async def _complete_login(
        self,
        user_id: str,
        login_submission: JsonDict,
        callback: Optional[Callable[[Dict[str, str]], Awaitable[None]]] = None,
        create_non_existent_users: bool = False,
        ratelimit: bool = True,
    ) -> 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
        all successful logins.

        Applies the ratelimiting for successful login attempts against an
        account.

        Args:
            user_id: ID of the user to register.
            login_submission: Dictionary of login information.
            callback: Callback function to run after login.
            create_non_existent_users: Whether to create the user if they don't
                exist. Defaults to False.
            ratelimit: Whether to ratelimit the login request.

        Returns:
            result: Dictionary of account information after successful login.
        """

        # Before we actually log them in we check if they've already logged in
        # too often. This happens here rather than before as we don't
        # necessarily know the user before now.
        if ratelimit:
            self._account_ratelimiter.ratelimit(user_id.lower())

        if create_non_existent_users:
            canonical_uid = await self.auth_handler.check_user_exists(user_id)
            if not canonical_uid:
                canonical_uid = await self.registration_handler.register_user(
                    localpart=UserID.from_string(user_id).localpart)
            user_id = canonical_uid

        device_id = login_submission.get("device_id")
        initial_display_name = login_submission.get(
            "initial_device_display_name")
        device_id, access_token = await self.registration_handler.register_device(
            user_id, device_id, initial_display_name)

        result = {
            "user_id": user_id,
            "access_token": access_token,
            "home_server": self.hs.hostname,
            "device_id": device_id,
        }

        if callback is not None:
            await callback(result)

        return result

    async def _do_token_login(self,
                              login_submission: JsonDict) -> Dict[str, str]:
        """
        Handle the final stage of SSO login.

        Args:
             login_submission: The JSON request body.

        Returns:
            The body of the JSON response.
        """
        token = login_submission["token"]
        auth_handler = self.auth_handler
        user_id = await auth_handler.validate_short_term_login_token_and_get_user_id(
            token)

        return await self._complete_login(
            user_id, login_submission, self.auth_handler._sso_login_callback)

    async def _do_jwt_login(self,
                            login_submission: JsonDict) -> Dict[str, str]:
        token = login_submission.get("token", None)
        if token is None:
            raise LoginError(403,
                             "Token field for JWT is missing",
                             errcode=Codes.FORBIDDEN)

        import jwt

        try:
            payload = jwt.decode(
                token,
                self.jwt_secret,
                algorithms=[self.jwt_algorithm],
                issuer=self.jwt_issuer,
                audience=self.jwt_audiences,
            )
        except jwt.PyJWTError as e:
            # A JWT error occurred, return some info back to the client.
            raise LoginError(
                403,
                "JWT validation failed: %s" % (str(e), ),
                errcode=Codes.FORBIDDEN,
            )

        user = payload.get("sub", None)
        if user is None:
            raise LoginError(403, "Invalid JWT", errcode=Codes.FORBIDDEN)

        user_id = UserID(user, self.hs.hostname).to_string()
        result = await self._complete_login(user_id,
                                            login_submission,
                                            create_non_existent_users=True)
        return result
Ejemplo n.º 2
0
class LoginRestServlet(RestServlet):
    PATTERNS = client_patterns("/login$", v1=True)
    CAS_TYPE = "m.login.cas"
    SSO_TYPE = "m.login.sso"
    TOKEN_TYPE = "m.login.token"
    JWT_TYPE = "m.login.jwt"

    def __init__(self, hs):
        super(LoginRestServlet, self).__init__()
        self.hs = hs
        self.jwt_enabled = hs.config.jwt_enabled
        self.jwt_secret = hs.config.jwt_secret
        self.jwt_algorithm = hs.config.jwt_algorithm
        self.saml2_enabled = hs.config.saml2_enabled
        self.cas_enabled = hs.config.cas_enabled
        self.auth_handler = self.hs.get_auth_handler()
        self.registration_handler = hs.get_registration_handler()
        self.handlers = hs.get_handlers()
        self._clock = hs.get_clock()
        self._well_known_builder = WellKnownBuilder(hs)
        self._address_ratelimiter = Ratelimiter()
        self._account_ratelimiter = Ratelimiter()
        self._failed_attempts_ratelimiter = Ratelimiter()

    def on_GET(self, request):
        flows = []
        if self.jwt_enabled:
            flows.append({"type": LoginRestServlet.JWT_TYPE})
        if self.saml2_enabled:
            flows.append({"type": LoginRestServlet.SSO_TYPE})
            flows.append({"type": LoginRestServlet.TOKEN_TYPE})
        if self.cas_enabled:
            flows.append({"type": LoginRestServlet.SSO_TYPE})

            # we advertise CAS for backwards compat, though MSC1721 renamed it
            # to SSO.
            flows.append({"type": LoginRestServlet.CAS_TYPE})

            # While its valid for us to advertise this login type generally,
            # synapse currently only gives out these tokens as part of the
            # CAS login flow.
            # Generally we don't want to advertise login flows that clients
            # don't know how to implement, since they (currently) will always
            # fall back to the fallback API if they don't understand one of the
            # login flow types returned.
            flows.append({"type": LoginRestServlet.TOKEN_TYPE})

        flows.extend(({
            "type": t
        } for t in self.auth_handler.get_supported_login_types()))

        return 200, {"flows": flows}

    def on_OPTIONS(self, request):
        return 200, {}

    async def on_POST(self, request):
        self._address_ratelimiter.ratelimit(
            request.getClientIP(),
            time_now_s=self.hs.clock.time(),
            rate_hz=self.hs.config.rc_login_address.per_second,
            burst_count=self.hs.config.rc_login_address.burst_count,
            update=True,
        )

        login_submission = parse_json_object_from_request(request)
        try:
            if self.jwt_enabled and (login_submission["type"]
                                     == LoginRestServlet.JWT_TYPE):
                result = await self.do_jwt_login(login_submission)
            elif login_submission["type"] == LoginRestServlet.TOKEN_TYPE:
                result = await self.do_token_login(login_submission)
            else:
                result = await self._do_other_login(login_submission)
        except KeyError:
            raise SynapseError(400, "Missing JSON keys.")

        well_known_data = self._well_known_builder.get_well_known()
        if well_known_data:
            result["well_known"] = well_known_data
        return 200, result

    async def _do_other_login(self, login_submission):
        """Handle non-token/saml/jwt logins

        Args:
            login_submission:

        Returns:
            dict: 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")

            if medium == "email":
                # For emails, transform the address to lowercase.
                # We store all email addreses as lowercase in the DB.
                # (See add_threepid in synapse/handlers/auth.py)
                address = address.lower()

            # 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),
                time_now_s=self._clock.time(),
                rate_hz=self.hs.config.rc_login_failed_attempts.per_second,
                burst_count=self.hs.config.rc_login_failed_attempts.
                burst_count,
                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),
                    time_now_s=self._clock.time(),
                    rate_hz=self.hs.config.rc_login_failed_attempts.per_second,
                    burst_count=self.hs.config.rc_login_failed_attempts.
                    burst_count,
                    update=True,
                )
                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(),
            time_now_s=self._clock.time(),
            rate_hz=self.hs.config.rc_login_failed_attempts.per_second,
            burst_count=self.hs.config.rc_login_failed_attempts.burst_count,
            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(),
                time_now_s=self._clock.time(),
                rate_hz=self.hs.config.rc_login_failed_attempts.per_second,
                burst_count=self.hs.config.rc_login_failed_attempts.
                burst_count,
                update=True,
            )
            raise

        result = await self._complete_login(canonical_user_id,
                                            login_submission, callback)
        return result

    async def _complete_login(self,
                              user_id,
                              login_submission,
                              callback=None,
                              create_non_existant_users=False):
        """Called when we've successfully authed the user and now need to
        actually login them in (e.g. create devices). This gets called on
        all succesful logins.

        Applies the ratelimiting for succesful login attempts against an
        account.

        Args:
            user_id (str): ID of the user to register.
            login_submission (dict): Dictionary of login information.
            callback (func|None): Callback function to run after registration.
            create_non_existant_users (bool): Whether to create the user if
                they don't exist. Defaults to False.

        Returns:
            result (Dict[str,str]): Dictionary of account information after
                successful registration.
        """

        # Before we actually log them in we check if they've already logged in
        # too often. This happens here rather than before as we don't
        # necessarily know the user before now.
        self._account_ratelimiter.ratelimit(
            user_id.lower(),
            time_now_s=self._clock.time(),
            rate_hz=self.hs.config.rc_login_account.per_second,
            burst_count=self.hs.config.rc_login_account.burst_count,
            update=True,
        )

        if create_non_existant_users:
            user_id = await self.auth_handler.check_user_exists(user_id)
            if not user_id:
                user_id = await self.registration_handler.register_user(
                    localpart=UserID.from_string(user_id).localpart)

        device_id = login_submission.get("device_id")
        initial_display_name = login_submission.get(
            "initial_device_display_name")
        device_id, access_token = await self.registration_handler.register_device(
            user_id, device_id, initial_display_name)

        result = {
            "user_id": user_id,
            "access_token": access_token,
            "home_server": self.hs.hostname,
            "device_id": device_id,
        }

        if callback is not None:
            await callback(result)

        return result

    async def do_token_login(self, login_submission):
        token = login_submission["token"]
        auth_handler = self.auth_handler
        user_id = await auth_handler.validate_short_term_login_token_and_get_user_id(
            token)

        result = await self._complete_login(user_id, login_submission)
        return result

    async def do_jwt_login(self, login_submission):
        token = login_submission.get("token", None)
        if token is None:
            raise LoginError(401,
                             "Token field for JWT is missing",
                             errcode=Codes.UNAUTHORIZED)

        import jwt
        from jwt.exceptions import InvalidTokenError

        try:
            payload = jwt.decode(token,
                                 self.jwt_secret,
                                 algorithms=[self.jwt_algorithm])
        except jwt.ExpiredSignatureError:
            raise LoginError(401, "JWT expired", errcode=Codes.UNAUTHORIZED)
        except InvalidTokenError:
            raise LoginError(401, "Invalid JWT", errcode=Codes.UNAUTHORIZED)

        user = payload.get("sub", None)
        if user is None:
            raise LoginError(401, "Invalid JWT", errcode=Codes.UNAUTHORIZED)

        user_id = UserID(user, self.hs.hostname).to_string()
        result = await self._complete_login(user_id,
                                            login_submission,
                                            create_non_existant_users=True)
        return result
Ejemplo n.º 3
0
class LoginRestServlet(RestServlet):
    PATTERNS = client_patterns("/login$", v1=True)
    CAS_TYPE = "m.login.cas"
    SSO_TYPE = "m.login.sso"
    TOKEN_TYPE = "m.login.token"
    JWT_TYPE = "m.login.jwt"

    def __init__(self, hs):
        super(LoginRestServlet, self).__init__()
        self.hs = hs
        self.jwt_enabled = hs.config.jwt_enabled
        self.jwt_secret = hs.config.jwt_secret
        self.jwt_algorithm = hs.config.jwt_algorithm
        self.saml2_enabled = hs.config.saml2_enabled
        self.cas_enabled = hs.config.cas_enabled
        self.auth_handler = self.hs.get_auth_handler()
        self.registration_handler = hs.get_registration_handler()
        self.handlers = hs.get_handlers()
        self._well_known_builder = WellKnownBuilder(hs)
        self._address_ratelimiter = Ratelimiter()

    def on_GET(self, request):
        flows = []
        if self.jwt_enabled:
            flows.append({"type": LoginRestServlet.JWT_TYPE})
        if self.saml2_enabled:
            flows.append({"type": LoginRestServlet.SSO_TYPE})
            flows.append({"type": LoginRestServlet.TOKEN_TYPE})
        if self.cas_enabled:
            flows.append({"type": LoginRestServlet.SSO_TYPE})

            # we advertise CAS for backwards compat, though MSC1721 renamed it
            # to SSO.
            flows.append({"type": LoginRestServlet.CAS_TYPE})

            # While its valid for us to advertise this login type generally,
            # synapse currently only gives out these tokens as part of the
            # CAS login flow.
            # Generally we don't want to advertise login flows that clients
            # don't know how to implement, since they (currently) will always
            # fall back to the fallback API if they don't understand one of the
            # login flow types returned.
            flows.append({"type": LoginRestServlet.TOKEN_TYPE})

        flows.extend(({
            "type": t
        } for t in self.auth_handler.get_supported_login_types()))

        return (200, {"flows": flows})

    def on_OPTIONS(self, request):
        return (200, {})

    @defer.inlineCallbacks
    def on_POST(self, request):
        self._address_ratelimiter.ratelimit(
            request.getClientIP(),
            time_now_s=self.hs.clock.time(),
            rate_hz=self.hs.config.rc_login_address.per_second,
            burst_count=self.hs.config.rc_login_address.burst_count,
            update=True,
        )

        login_submission = parse_json_object_from_request(request)
        try:
            if self.jwt_enabled and (login_submission["type"]
                                     == LoginRestServlet.JWT_TYPE):
                result = yield self.do_jwt_login(login_submission)
            elif login_submission["type"] == LoginRestServlet.TOKEN_TYPE:
                result = yield self.do_token_login(login_submission)
            else:
                result = yield self._do_other_login(login_submission)
        except KeyError:
            raise SynapseError(400, "Missing JSON keys.")

        well_known_data = self._well_known_builder.get_well_known()
        if well_known_data:
            result["well_known"] = well_known_data
        return (200, result)

    @defer.inlineCallbacks
    def _do_other_login(self, login_submission):
        """Handle non-token/saml/jwt logins

        Args:
            login_submission:

        Returns:
            dict: 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")

            if medium == "email":
                # For emails, transform the address to lowercase.
                # We store all email addreses as lowercase in the DB.
                # (See add_threepid in synapse/handlers/auth.py)
                address = address.lower()

            # Check for login providers that support 3pid login types
            canonical_user_id, callback_3pid = (
                yield self.auth_handler.check_password_provider_3pid(
                    medium, address, login_submission["password"]))
            if canonical_user_id:
                # Authentication through password provider and 3pid succeeded
                result = yield self._register_device_with_callback(
                    canonical_user_id, login_submission, callback_3pid)
                return result

            # No password providers were able to handle this 3pid
            # Check local store
            user_id = yield self.hs.get_datastore().get_user_id_by_threepid(
                medium, address)
            if not user_id:
                logger.warn("unknown 3pid identifier medium %s, address %r",
                            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")

        canonical_user_id, callback = yield self.auth_handler.validate_login(
            identifier["user"], login_submission)

        result = yield self._register_device_with_callback(
            canonical_user_id, login_submission, callback)
        return result

    @defer.inlineCallbacks
    def _register_device_with_callback(self,
                                       user_id,
                                       login_submission,
                                       callback=None):
        """ Registers a device with a given user_id. Optionally run a callback
        function after registration has completed.

        Args:
            user_id (str): ID of the user to register.
            login_submission (dict): Dictionary of login information.
            callback (func|None): Callback function to run after registration.

        Returns:
            result (Dict[str,str]): Dictionary of account information after
                successful registration.
        """
        device_id = login_submission.get("device_id")
        initial_display_name = login_submission.get(
            "initial_device_display_name")
        device_id, access_token = yield self.registration_handler.register_device(
            user_id, device_id, initial_display_name)

        result = {
            "user_id": user_id,
            "access_token": access_token,
            "home_server": self.hs.hostname,
            "device_id": device_id,
        }

        if callback is not None:
            yield callback(result)

        return result

    @defer.inlineCallbacks
    def do_token_login(self, login_submission):
        token = login_submission["token"]
        auth_handler = self.auth_handler
        user_id = (yield auth_handler.
                   validate_short_term_login_token_and_get_user_id(token))

        result = yield self._register_device_with_callback(
            user_id, login_submission)
        return result

    @defer.inlineCallbacks
    def do_jwt_login(self, login_submission):
        token = login_submission.get("token", None)
        if token is None:
            raise LoginError(401,
                             "Token field for JWT is missing",
                             errcode=Codes.UNAUTHORIZED)

        import jwt
        from jwt.exceptions import InvalidTokenError

        try:
            payload = jwt.decode(token,
                                 self.jwt_secret,
                                 algorithms=[self.jwt_algorithm])
        except jwt.ExpiredSignatureError:
            raise LoginError(401, "JWT expired", errcode=Codes.UNAUTHORIZED)
        except InvalidTokenError:
            raise LoginError(401, "Invalid JWT", errcode=Codes.UNAUTHORIZED)

        user = payload.get("sub", None)
        if user is None:
            raise LoginError(401, "Invalid JWT", errcode=Codes.UNAUTHORIZED)

        user_id = UserID(user, self.hs.hostname).to_string()

        registered_user_id = yield self.auth_handler.check_user_exists(user_id)
        if not registered_user_id:
            registered_user_id = yield self.registration_handler.register_user(
                localpart=user)

        result = yield self._register_device_with_callback(
            registered_user_id, login_submission)
        return result
Ejemplo n.º 4
0
class LoginRestServlet(ClientV1RestServlet):
    PATTERNS = client_path_patterns("/login$")
    CAS_TYPE = "m.login.cas"
    SSO_TYPE = "m.login.sso"
    TOKEN_TYPE = "m.login.token"
    JWT_TYPE = "m.login.jwt"

    def __init__(self, hs):
        super(LoginRestServlet, self).__init__(hs)
        self.jwt_enabled = hs.config.jwt_enabled
        self.jwt_secret = hs.config.jwt_secret
        self.jwt_algorithm = hs.config.jwt_algorithm
        self.cas_enabled = hs.config.cas_enabled
        self.auth_handler = self.hs.get_auth_handler()
        self.registration_handler = hs.get_registration_handler()
        self.handlers = hs.get_handlers()
        self._well_known_builder = WellKnownBuilder(hs)
        self._address_ratelimiter = Ratelimiter()

    def on_GET(self, request):
        flows = []
        if self.jwt_enabled:
            flows.append({"type": LoginRestServlet.JWT_TYPE})
        if self.cas_enabled:
            flows.append({"type": LoginRestServlet.SSO_TYPE})

            # we advertise CAS for backwards compat, though MSC1721 renamed it
            # to SSO.
            flows.append({"type": LoginRestServlet.CAS_TYPE})

            # While its valid for us to advertise this login type generally,
            # synapse currently only gives out these tokens as part of the
            # CAS login flow.
            # Generally we don't want to advertise login flows that clients
            # don't know how to implement, since they (currently) will always
            # fall back to the fallback API if they don't understand one of the
            # login flow types returned.
            flows.append({"type": LoginRestServlet.TOKEN_TYPE})

        flows.extend((
            {"type": t} for t in self.auth_handler.get_supported_login_types()
        ))

        return (200, {"flows": flows})

    def on_OPTIONS(self, request):
        return (200, {})

    @defer.inlineCallbacks
    def on_POST(self, request):
        self._address_ratelimiter.ratelimit(
            request.getClientIP(), time_now_s=self.hs.clock.time(),
            rate_hz=self.hs.config.rc_login_address.per_second,
            burst_count=self.hs.config.rc_login_address.burst_count,
            update=True,
        )

        login_submission = parse_json_object_from_request(request)
        try:
            if self.jwt_enabled and (login_submission["type"] ==
                                     LoginRestServlet.JWT_TYPE):
                result = yield self.do_jwt_login(login_submission)
            elif login_submission["type"] == LoginRestServlet.TOKEN_TYPE:
                result = yield self.do_token_login(login_submission)
            else:
                result = yield self._do_other_login(login_submission)
        except KeyError:
            raise SynapseError(400, "Missing JSON keys.")

        well_known_data = self._well_known_builder.get_well_known()
        if well_known_data:
            result["well_known"] = well_known_data
        defer.returnValue((200, result))

    @defer.inlineCallbacks
    def _do_other_login(self, login_submission):
        """Handle non-token/saml/jwt logins

        Args:
            login_submission:

        Returns:
            dict: 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")

            if medium == 'email':
                # For emails, transform the address to lowercase.
                # We store all email addreses as lowercase in the DB.
                # (See add_threepid in synapse/handlers/auth.py)
                address = address.lower()

            # Check for login providers that support 3pid login types
            canonical_user_id, callback_3pid = (
                yield self.auth_handler.check_password_provider_3pid(
                    medium,
                    address,
                    login_submission["password"],
                )
            )
            if canonical_user_id:
                # Authentication through password provider and 3pid succeeded
                result = yield self._register_device_with_callback(
                    canonical_user_id, login_submission, callback_3pid,
                )
                defer.returnValue(result)

            # No password providers were able to handle this 3pid
            # Check local store
            user_id = yield self.hs.get_datastore().get_user_id_by_threepid(
                medium, address,
            )
            if not user_id:
                logger.warn(
                    "unknown 3pid identifier medium %s, address %r",
                    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")

        canonical_user_id, callback = yield self.auth_handler.validate_login(
            identifier["user"],
            login_submission,
        )

        result = yield self._register_device_with_callback(
            canonical_user_id, login_submission, callback,
        )
        defer.returnValue(result)

    @defer.inlineCallbacks
    def _register_device_with_callback(
        self,
        user_id,
        login_submission,
        callback=None,
    ):
        """ Registers a device with a given user_id. Optionally run a callback
        function after registration has completed.

        Args:
            user_id (str): ID of the user to register.
            login_submission (dict): Dictionary of login information.
            callback (func|None): Callback function to run after registration.

        Returns:
            result (Dict[str,str]): Dictionary of account information after
                successful registration.
        """
        device_id = login_submission.get("device_id")
        initial_display_name = login_submission.get("initial_device_display_name")
        device_id, access_token = yield self.registration_handler.register_device(
            user_id, device_id, initial_display_name,
        )

        result = {
            "user_id": user_id,
            "access_token": access_token,
            "home_server": self.hs.hostname,
            "device_id": device_id,
        }

        if callback is not None:
            yield callback(result)

        defer.returnValue(result)

    @defer.inlineCallbacks
    def do_token_login(self, login_submission):
        token = login_submission['token']
        auth_handler = self.auth_handler
        user_id = (
            yield auth_handler.validate_short_term_login_token_and_get_user_id(token)
        )

        device_id = login_submission.get("device_id")
        initial_display_name = login_submission.get("initial_device_display_name")
        device_id, access_token = yield self.registration_handler.register_device(
            user_id, device_id, initial_display_name,
        )

        result = {
            "user_id": user_id,  # may have changed
            "access_token": access_token,
            "home_server": self.hs.hostname,
            "device_id": device_id,
        }

        defer.returnValue(result)

    @defer.inlineCallbacks
    def do_jwt_login(self, login_submission):
        token = login_submission.get("token", None)
        if token is None:
            raise LoginError(
                401, "Token field for JWT is missing",
                errcode=Codes.UNAUTHORIZED
            )

        import jwt
        from jwt.exceptions import InvalidTokenError

        try:
            payload = jwt.decode(token, self.jwt_secret, algorithms=[self.jwt_algorithm])
        except jwt.ExpiredSignatureError:
            raise LoginError(401, "JWT expired", errcode=Codes.UNAUTHORIZED)
        except InvalidTokenError:
            raise LoginError(401, "Invalid JWT", errcode=Codes.UNAUTHORIZED)

        user = payload.get("sub", None)
        if user is None:
            raise LoginError(401, "Invalid JWT", errcode=Codes.UNAUTHORIZED)

        user_id = UserID(user, self.hs.hostname).to_string()

        auth_handler = self.auth_handler
        registered_user_id = yield auth_handler.check_user_exists(user_id)
        if registered_user_id:
            device_id = login_submission.get("device_id")
            initial_display_name = login_submission.get("initial_device_display_name")
            device_id, access_token = yield self.registration_handler.register_device(
                registered_user_id, device_id, initial_display_name,
            )

            result = {
                "user_id": registered_user_id,
                "access_token": access_token,
                "home_server": self.hs.hostname,
            }
        else:
            user_id, access_token = (
                yield self.handlers.registration_handler.register(localpart=user)
            )

            device_id = login_submission.get("device_id")
            initial_display_name = login_submission.get("initial_device_display_name")
            device_id, access_token = yield self.registration_handler.register_device(
                registered_user_id, device_id, initial_display_name,
            )

            result = {
                "user_id": user_id,  # may have changed
                "access_token": access_token,
                "home_server": self.hs.hostname,
            }

        defer.returnValue(result)
Ejemplo n.º 5
0
class LoginRestServlet(RestServlet):
    PATTERNS = client_patterns("/login$", v1=True)
    CAS_TYPE = "m.login.cas"
    SSO_TYPE = "m.login.sso"
    TOKEN_TYPE = "m.login.token"
    JWT_TYPE = "org.matrix.login.jwt"
    APPSERVICE_TYPE = "m.login.application_service"
    REFRESH_TOKEN_PARAM = "refresh_token"

    def __init__(self, hs: "HomeServer"):
        super().__init__()
        self.hs = hs

        # JWT configuration variables.
        self.jwt_enabled = hs.config.jwt.jwt_enabled
        self.jwt_secret = hs.config.jwt.jwt_secret
        self.jwt_subject_claim = hs.config.jwt.jwt_subject_claim
        self.jwt_algorithm = hs.config.jwt.jwt_algorithm
        self.jwt_issuer = hs.config.jwt.jwt_issuer
        self.jwt_audiences = hs.config.jwt.jwt_audiences

        # SSO configuration.
        self.saml2_enabled = hs.config.saml2.saml2_enabled
        self.cas_enabled = hs.config.cas.cas_enabled
        self.oidc_enabled = hs.config.oidc.oidc_enabled
        self._refresh_tokens_enabled = (
            hs.config.registration.refreshable_access_token_lifetime
            is not None)

        self.auth = hs.get_auth()

        self.clock = hs.get_clock()

        self.auth_handler = self.hs.get_auth_handler()
        self.registration_handler = hs.get_registration_handler()
        self._sso_handler = hs.get_sso_handler()

        self._well_known_builder = WellKnownBuilder(hs)
        self._address_ratelimiter = Ratelimiter(
            store=hs.get_datastores().main,
            clock=hs.get_clock(),
            rate_hz=self.hs.config.ratelimiting.rc_login_address.per_second,
            burst_count=self.hs.config.ratelimiting.rc_login_address.
            burst_count,
        )
        self._account_ratelimiter = Ratelimiter(
            store=hs.get_datastores().main,
            clock=hs.get_clock(),
            rate_hz=self.hs.config.ratelimiting.rc_login_account.per_second,
            burst_count=self.hs.config.ratelimiting.rc_login_account.
            burst_count,
        )

        # ensure the CAS/SAML/OIDC handlers are loaded on this worker instance.
        # The reason for this is to ensure that the auth_provider_ids are registered
        # with SsoHandler, which in turn ensures that the login/registration prometheus
        # counters are initialised for the auth_provider_ids.
        _load_sso_handlers(hs)

    def on_GET(self, request: SynapseRequest) -> Tuple[int, JsonDict]:
        flows: List[JsonDict] = []
        if self.jwt_enabled:
            flows.append({"type": LoginRestServlet.JWT_TYPE})

        if self.cas_enabled:
            # we advertise CAS for backwards compat, though MSC1721 renamed it
            # to SSO.
            flows.append({"type": LoginRestServlet.CAS_TYPE})

        if self.cas_enabled or self.saml2_enabled or self.oidc_enabled:
            flows.append({
                "type":
                LoginRestServlet.SSO_TYPE,
                "identity_providers": [
                    _get_auth_flow_dict_for_idp(idp) for idp in
                    self._sso_handler.get_identity_providers().values()
                ],
            })

            # While it's valid for us to advertise this login type generally,
            # synapse currently only gives out these tokens as part of the
            # SSO login flow.
            # Generally we don't want to advertise login flows that clients
            # don't know how to implement, since they (currently) will always
            # fall back to the fallback API if they don't understand one of the
            # login flow types returned.
            flows.append({"type": LoginRestServlet.TOKEN_TYPE})

        flows.extend({"type": t}
                     for t in self.auth_handler.get_supported_login_types())

        flows.append({"type": LoginRestServlet.APPSERVICE_TYPE})

        return 200, {"flows": flows}

    async def on_POST(self,
                      request: SynapseRequest) -> Tuple[int, LoginResponse]:
        login_submission = parse_json_object_from_request(request)

        # Check to see if the client requested a refresh token.
        client_requested_refresh_token = login_submission.get(
            LoginRestServlet.REFRESH_TOKEN_PARAM, False)
        if not isinstance(client_requested_refresh_token, bool):
            raise SynapseError(400, "`refresh_token` should be true or false.")

        should_issue_refresh_token = (self._refresh_tokens_enabled
                                      and client_requested_refresh_token)

        try:
            if login_submission["type"] == LoginRestServlet.APPSERVICE_TYPE:
                appservice = self.auth.get_appservice_by_req(request)

                if appservice.is_rate_limited():
                    await self._address_ratelimiter.ratelimit(
                        None,
                        request.getClientAddress().host)

                result = await self._do_appservice_login(
                    login_submission,
                    appservice,
                    should_issue_refresh_token=should_issue_refresh_token,
                )
            elif (self.jwt_enabled
                  and login_submission["type"] == LoginRestServlet.JWT_TYPE):
                await self._address_ratelimiter.ratelimit(
                    None,
                    request.getClientAddress().host)
                result = await self._do_jwt_login(
                    login_submission,
                    should_issue_refresh_token=should_issue_refresh_token,
                )
            elif login_submission["type"] == LoginRestServlet.TOKEN_TYPE:
                await self._address_ratelimiter.ratelimit(
                    None,
                    request.getClientAddress().host)
                result = await self._do_token_login(
                    login_submission,
                    should_issue_refresh_token=should_issue_refresh_token,
                )
            else:
                await self._address_ratelimiter.ratelimit(
                    None,
                    request.getClientAddress().host)
                result = await self._do_other_login(
                    login_submission,
                    should_issue_refresh_token=should_issue_refresh_token,
                )
        except KeyError:
            raise SynapseError(400, "Missing JSON keys.")

        well_known_data = self._well_known_builder.get_well_known()
        if well_known_data:
            result["well_known"] = well_known_data
        return 200, result

    async def _do_appservice_login(
        self,
        login_submission: JsonDict,
        appservice: ApplicationService,
        should_issue_refresh_token: bool = False,
    ) -> LoginResponse:
        identifier = login_submission.get("identifier")
        logger.info("Got appservice login request with identifier: %r",
                    identifier)

        if not isinstance(identifier, dict):
            raise SynapseError(400, "Invalid identifier in login submission",
                               Codes.INVALID_PARAM)

        # this login flow only supports identifiers of type "m.id.user".
        if identifier.get("type") != "m.id.user":
            raise SynapseError(400, "Unknown login identifier type",
                               Codes.INVALID_PARAM)

        user = identifier.get("user")
        if not isinstance(user, str):
            raise SynapseError(400, "Invalid user in identifier",
                               Codes.INVALID_PARAM)

        if user.startswith("@"):
            qualified_user_id = user
        else:
            qualified_user_id = UserID(user, self.hs.hostname).to_string()

        if not appservice.is_interested_in_user(qualified_user_id):
            raise LoginError(403,
                             "Invalid access_token",
                             errcode=Codes.FORBIDDEN)

        return await self._complete_login(
            qualified_user_id,
            login_submission,
            ratelimit=appservice.is_rate_limited(),
            should_issue_refresh_token=should_issue_refresh_token,
        )

    async def _do_other_login(
            self,
            login_submission: JsonDict,
            should_issue_refresh_token: bool = False) -> LoginResponse:
        """Handle non-token/saml/jwt logins

        Args:
            login_submission:
            should_issue_refresh_token: True if this login should issue
                a refresh token alongside the access token.

        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"),
        )
        canonical_user_id, callback = await self.auth_handler.validate_login(
            login_submission, ratelimit=True)
        result = await self._complete_login(
            canonical_user_id,
            login_submission,
            callback,
            should_issue_refresh_token=should_issue_refresh_token,
        )
        return result

    async def _complete_login(
        self,
        user_id: str,
        login_submission: JsonDict,
        callback: Optional[Callable[[LoginResponse], Awaitable[None]]] = None,
        create_non_existent_users: bool = False,
        ratelimit: bool = True,
        auth_provider_id: Optional[str] = None,
        should_issue_refresh_token: bool = False,
        auth_provider_session_id: Optional[str] = None,
    ) -> LoginResponse:
        """Called when we've successfully authed the user and now need to
        actually login them in (e.g. create devices). This gets called on
        all successful logins.

        Applies the ratelimiting for successful login attempts against an
        account.

        Args:
            user_id: ID of the user to register.
            login_submission: Dictionary of login information.
            callback: Callback function to run after login.
            create_non_existent_users: Whether to create the user if they don't
                exist. Defaults to False.
            ratelimit: Whether to ratelimit the login request.
            auth_provider_id: The SSO IdP the user used, if any.
            should_issue_refresh_token: True if this login should issue
                a refresh token alongside the access token.
            auth_provider_session_id: The session ID got during login from the SSO IdP.

        Returns:
            result: Dictionary of account information after successful login.
        """

        # Before we actually log them in we check if they've already logged in
        # too often. This happens here rather than before as we don't
        # necessarily know the user before now.
        if ratelimit:
            await self._account_ratelimiter.ratelimit(None, user_id.lower())

        if create_non_existent_users:
            canonical_uid = await self.auth_handler.check_user_exists(user_id)
            if not canonical_uid:
                canonical_uid = await self.registration_handler.register_user(
                    localpart=UserID.from_string(user_id).localpart)
            user_id = canonical_uid

        device_id = login_submission.get("device_id")

        # If device_id is present, check that device_id is not longer than a reasonable 512 characters
        if device_id and len(device_id) > 512:
            raise LoginError(
                400,
                "device_id cannot be longer than 512 characters.",
                errcode=Codes.INVALID_PARAM,
            )

        initial_display_name = login_submission.get(
            "initial_device_display_name")
        (
            device_id,
            access_token,
            valid_until_ms,
            refresh_token,
        ) = await self.registration_handler.register_device(
            user_id,
            device_id,
            initial_display_name,
            auth_provider_id=auth_provider_id,
            should_issue_refresh_token=should_issue_refresh_token,
            auth_provider_session_id=auth_provider_session_id,
        )

        result = LoginResponse(
            user_id=user_id,
            access_token=access_token,
            home_server=self.hs.hostname,
            device_id=device_id,
        )

        if valid_until_ms is not None:
            expires_in_ms = valid_until_ms - self.clock.time_msec()
            result["expires_in_ms"] = expires_in_ms

        if refresh_token is not None:
            result["refresh_token"] = refresh_token

        if callback is not None:
            await callback(result)

        return result

    async def _do_token_login(
            self,
            login_submission: JsonDict,
            should_issue_refresh_token: bool = False) -> LoginResponse:
        """
        Handle the final stage of SSO login.

        Args:
            login_submission: The JSON request body.
            should_issue_refresh_token: True if this login should issue
                a refresh token alongside the access token.

        Returns:
            The body of the JSON response.
        """
        token = login_submission["token"]
        auth_handler = self.auth_handler
        res = await auth_handler.validate_short_term_login_token(token)

        return await self._complete_login(
            res.user_id,
            login_submission,
            self.auth_handler._sso_login_callback,
            auth_provider_id=res.auth_provider_id,
            should_issue_refresh_token=should_issue_refresh_token,
            auth_provider_session_id=res.auth_provider_session_id,
        )

    async def _do_jwt_login(
            self,
            login_submission: JsonDict,
            should_issue_refresh_token: bool = False) -> LoginResponse:
        token = login_submission.get("token", None)
        if token is None:
            raise LoginError(403,
                             "Token field for JWT is missing",
                             errcode=Codes.FORBIDDEN)

        from authlib.jose import JsonWebToken, JWTClaims
        from authlib.jose.errors import BadSignatureError, InvalidClaimError, JoseError

        jwt = JsonWebToken([self.jwt_algorithm])
        claim_options = {}
        if self.jwt_issuer is not None:
            claim_options["iss"] = {
                "value": self.jwt_issuer,
                "essential": True
            }
        if self.jwt_audiences is not None:
            claim_options["aud"] = {
                "values": self.jwt_audiences,
                "essential": True
            }

        try:
            claims = jwt.decode(
                token,
                key=self.jwt_secret,
                claims_cls=JWTClaims,
                claims_options=claim_options,
            )
        except BadSignatureError:
            # We handle this case separately to provide a better error message
            raise LoginError(
                403,
                "JWT validation failed: Signature verification failed",
                errcode=Codes.FORBIDDEN,
            )
        except JoseError as e:
            # A JWT error occurred, return some info back to the client.
            raise LoginError(
                403,
                "JWT validation failed: %s" % (str(e), ),
                errcode=Codes.FORBIDDEN,
            )

        try:
            claims.validate(leeway=120)  # allows 2 min of clock skew

            # Enforce the old behavior which is rolled out in productive
            # servers: if the JWT contains an 'aud' claim but none is
            # configured, the login attempt will fail
            if claims.get("aud") is not None:
                if self.jwt_audiences is None or len(self.jwt_audiences) == 0:
                    raise InvalidClaimError("aud")
        except JoseError as e:
            raise LoginError(
                403,
                "JWT validation failed: %s" % (str(e), ),
                errcode=Codes.FORBIDDEN,
            )

        user = claims.get(self.jwt_subject_claim, None)
        if user is None:
            raise LoginError(403, "Invalid JWT", errcode=Codes.FORBIDDEN)

        user_id = UserID(user, self.hs.hostname).to_string()
        result = await self._complete_login(
            user_id,
            login_submission,
            create_non_existent_users=True,
            should_issue_refresh_token=should_issue_refresh_token,
        )
        return result
Ejemplo n.º 6
0
class LoginRestServlet(RestServlet):
    PATTERNS = client_patterns("/login$", v1=True)
    CAS_TYPE = "m.login.cas"
    SSO_TYPE = "m.login.sso"
    TOKEN_TYPE = "m.login.token"
    JWT_TYPE = "org.matrix.login.jwt"
    JWT_TYPE_DEPRECATED = "m.login.jwt"
    APPSERVICE_TYPE = "uk.half-shot.msc2778.login.application_service"

    # add new Type
    VER_CODE_EMAIL_TYPE = "m.login.verCode.email"
    VER_CODE_MSISDN_TYPE = "m.login.verCode.msisdn"
    OAUTH2_ALIPAY_TYPE = "m.login.OAuth2.alipay"
    OAUTH2_WEIXIN_TYPE = "m.login.OAuth2.weixin"
    SSO_LDAP_TYPE = "m.login.sso.ldap"

    def __init__(self, hs: "HomeServer"):
        super().__init__()
        self.hs = hs

        # JWT configuration variables.
        self.jwt_enabled = hs.config.jwt_enabled
        self.jwt_secret = hs.config.jwt_secret
        self.jwt_algorithm = hs.config.jwt_algorithm
        self.jwt_issuer = hs.config.jwt_issuer
        self.jwt_audiences = hs.config.jwt_audiences

        # SSO configuration.
        self.saml2_enabled = hs.config.saml2_enabled
        self.cas_enabled = hs.config.cas_enabled
        self.oidc_enabled = hs.config.oidc_enabled
        self._msc2858_enabled = hs.config.experimental.msc2858_enabled

        self.auth = hs.get_auth()

        self.auth_handler = self.hs.get_auth_handler()
        self.registration_handler = hs.get_registration_handler()
        self._sso_handler = hs.get_sso_handler()

        self._well_known_builder = WellKnownBuilder(hs)
        self._address_ratelimiter = Ratelimiter(
            clock=hs.get_clock(),
            rate_hz=self.hs.config.rc_login_address.per_second,
            burst_count=self.hs.config.rc_login_address.burst_count,
        )
        self._account_ratelimiter = Ratelimiter(
            clock=hs.get_clock(),
            rate_hz=self.hs.config.rc_login_account.per_second,
            burst_count=self.hs.config.rc_login_account.burst_count,
        )
        self.http_client = SimpleHttpClient(hs)
        self._cache = hs.get_eachchat_cache_for_openid()

    def on_GET(self, request: SynapseRequest):
        flows = []
        if self.jwt_enabled:
            flows.append({"type": LoginRestServlet.JWT_TYPE})
            flows.append({"type": LoginRestServlet.JWT_TYPE_DEPRECATED})

        if self.cas_enabled:
            # we advertise CAS for backwards compat, though MSC1721 renamed it
            # to SSO.
            flows.append({"type": LoginRestServlet.CAS_TYPE})

        if self.cas_enabled or self.saml2_enabled or self.oidc_enabled:
            sso_flow = {"type": LoginRestServlet.SSO_TYPE}  # type: JsonDict

            if self._msc2858_enabled:
                sso_flow["org.matrix.msc2858.identity_providers"] = [
                    _get_auth_flow_dict_for_idp(idp) for idp in
                    self._sso_handler.get_identity_providers().values()
                ]

            flows.append(sso_flow)

            # While it's valid for us to advertise this login type generally,
            # synapse currently only gives out these tokens as part of the
            # SSO login flow.
            # Generally we don't want to advertise login flows that clients
            # don't know how to implement, since they (currently) will always
            # fall back to the fallback API if they don't understand one of the
            # login flow types returned.
            flows.append({"type": LoginRestServlet.TOKEN_TYPE})

        flows.extend(({
            "type": t
        } for t in self.auth_handler.get_supported_login_types()))

        flows.append({"type": LoginRestServlet.APPSERVICE_TYPE})

        flows.append({"type": LoginRestServlet.VER_CODE_EMAIL_TYPE})
        flows.append({"type": LoginRestServlet.VER_CODE_MSISDN_TYPE})
        flows.append({"type": LoginRestServlet.OAUTH2_ALIPAY_TYPE})
        flows.append({"type": LoginRestServlet.OAUTH2_WEIXIN_TYPE})
        flows.append({"type": LoginRestServlet.SSO_LDAP_TYPE})

        return 200, {"flows": flows}

    async def on_POST(self, request: SynapseRequest):
        login_submission = parse_json_object_from_request(request)
        logger.info('login type------%s' % (login_submission["type"], ))
        try:
            if login_submission["type"] == LoginRestServlet.APPSERVICE_TYPE:
                appservice = self.auth.get_appservice_by_req(request)

                if appservice.is_rate_limited():
                    self._address_ratelimiter.ratelimit(request.getClientIP())

                result = await self._do_appservice_login(
                    login_submission, appservice)
            elif self.jwt_enabled and (
                    login_submission["type"] == LoginRestServlet.JWT_TYPE
                    or login_submission["type"]
                    == LoginRestServlet.JWT_TYPE_DEPRECATED):
                self._address_ratelimiter.ratelimit(request.getClientIP())
                result = await self._do_jwt_login(login_submission)
            elif login_submission["type"] == LoginRestServlet.TOKEN_TYPE:
                self._address_ratelimiter.ratelimit(request.getClientIP())
                result = await self._do_token_login(login_submission)
            elif login_submission[
                    "type"] == LoginRestServlet.VER_CODE_EMAIL_TYPE:  # Email verification code to login
                result = await self._do_ver_code_email_login(login_submission)
            elif login_submission[
                    "type"] == LoginRestServlet.VER_CODE_MSISDN_TYPE:  # Msisdn verification code to login
                result = await self._do_ver_code_msisdn_login(login_submission)
            elif login_submission[
                    "type"] == LoginRestServlet.OAUTH2_ALIPAY_TYPE:  # OAuth2 of alipay login
                result = await self._do_oauth2_login(login_submission)
            elif login_submission[
                    "type"] == LoginRestServlet.OAUTH2_WEIXIN_TYPE:  # OAuth2 of weixin login
                result = await self._do_oauth2_login(login_submission)
            elif login_submission[
                    "type"] == LoginRestServlet.SSO_LDAP_TYPE:  # SSO of ldap login
                result = await self._do_sso_ldap_login(login_submission)
            else:
                self._address_ratelimiter.ratelimit(request.getClientIP())
                result = await self._do_other_login(login_submission)
        except KeyError:
            raise SynapseError(400, "Missing JSON keys.")

        well_known_data = self._well_known_builder.get_well_known()
        if well_known_data:
            result["well_known"] = well_known_data
        return 200, result

    async def _do_appservice_login(self, login_submission: JsonDict,
                                   appservice: ApplicationService):
        identifier = login_submission.get("identifier")
        logger.info("Got appservice login request with identifier: %r",
                    identifier)

        if not isinstance(identifier, dict):
            raise SynapseError(400, "Invalid identifier in login submission",
                               Codes.INVALID_PARAM)

        # this login flow only supports identifiers of type "m.id.user".
        if identifier.get("type") != "m.id.user":
            raise SynapseError(400, "Unknown login identifier type",
                               Codes.INVALID_PARAM)

        user = identifier.get("user")
        if not isinstance(user, str):
            raise SynapseError(400, "Invalid user in identifier",
                               Codes.INVALID_PARAM)

        if user.startswith("@"):
            qualified_user_id = user
        else:
            qualified_user_id = UserID(user, self.hs.hostname).to_string()

        if not appservice.is_interested_in_user(qualified_user_id):
            raise LoginError(403,
                             "Invalid access_token",
                             errcode=Codes.FORBIDDEN)

        return await self._complete_login(
            qualified_user_id,
            login_submission,
            ratelimit=appservice.is_rate_limited())

    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"),
        )
        canonical_user_id, callback = await self.auth_handler.validate_login(
            login_submission, ratelimit=True)
        result = await self._complete_login(canonical_user_id,
                                            login_submission, callback)
        return result

    async def _complete_login(
        self,
        user_id: str,
        login_submission: JsonDict,
        callback: Optional[Callable[[Dict[str, str]], Awaitable[None]]] = None,
        create_non_existent_users: bool = False,
        ratelimit: bool = True,
    ) -> 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
        all successful logins.

        Applies the ratelimiting for successful login attempts against an
        account.

        Args:
            user_id: ID of the user to register.
            login_submission: Dictionary of login information.
            callback: Callback function to run after login.
            create_non_existent_users: Whether to create the user if they don't
                exist. Defaults to False.
            ratelimit: Whether to ratelimit the login request.

        Returns:
            result: Dictionary of account information after successful login.
        """

        # Before we actually log them in we check if they've already logged in
        # too often. This happens here rather than before as we don't
        # necessarily know the user before now.
        if ratelimit:
            self._account_ratelimiter.ratelimit(user_id.lower())

        if create_non_existent_users:
            canonical_uid = await self.auth_handler.check_user_exists(user_id)
            if not canonical_uid:
                canonical_uid = await self.registration_handler.register_user(
                    localpart=UserID.from_string(user_id).localpart)
            user_id = canonical_uid

        device_id = login_submission.get("device_id")
        initial_display_name = login_submission.get(
            "initial_device_display_name")
        device_id, access_token = await self.registration_handler.register_device(
            user_id, device_id, initial_display_name)

        result = {
            "user_id": user_id,
            "access_token": access_token,
            "home_server": self.hs.hostname,
            "device_id": device_id,
        }

        if callback is not None:
            await callback(result)

        return result

    async def _do_token_login(self,
                              login_submission: JsonDict) -> Dict[str, str]:
        """
        Handle the final stage of SSO login.

        Args:
             login_submission: The JSON request body.

        Returns:
            The body of the JSON response.
        """
        token = login_submission["token"]
        auth_handler = self.auth_handler
        user_id = await auth_handler.validate_short_term_login_token_and_get_user_id(
            token)

        return await self._complete_login(
            user_id, login_submission, self.auth_handler._sso_login_callback)

    # the login of email verification code
    async def _do_ver_code_email_login(
            self, login_submission: JsonDict) -> Dict[str, str]:
        email = login_submission.get("email", None)
        if email is None:
            raise LoginError(410,
                             "Email field for ver_code_email is missing",
                             errcode=Codes.FORBIDDEN)
        # verify email and send to email
        user_id = await self.hs.get_datastore().get_user_id_by_threepid(
            "email", email)
        if user_id is None:
            raise SynapseError(400, "Email not found",
                               Codes.THREEPID_NOT_FOUND)

        ver_code = login_submission.get("ver_code", None)
        if ver_code is None:
            raise LoginError(410,
                             "ver_code field for ver_code_email is missing",
                             errcode=Codes.FORBIDDEN)

        # ver_code_service_host = "192.168.0.4"
        # ver_code_service_port = "8080"
        # ver_code_service_validation_api = "/api/services/auth/v1/code/validation"
        params = {"value": email, "type": "email", "code": ver_code}
        try:
            ver_code_res = await self.http_client.post_json_get_json(
                self.hs.config.auth_baseurl +
                self.hs.config.auth_code_validation,
                params,
            )
            logger.info("email ver_code_res: %s" % (str(ver_code_res)))
            if ver_code_res["code"] != 200:
                raise LoginError(412,
                                 "ver_code invalid",
                                 errcode=Codes.FORBIDDEN)
        except HttpResponseException as e:
            logger.info("Proxied validation vercode failed: %r", e)
            raise e.to_synapse_error()
        except RequestTimedOutError:
            raise SynapseError(
                500,
                "Timed out contacting extral server:ver_code_send_service")

        # lookup cache_ver_code from redis by email
        # self.hs.get_redis
        # ver_code == cache_ver_code

        # call IS for verify email ver_code
        # identity_handler = self.hs.get_identity_handler()
        # result = await identity_handler.request_validate_threepid_ver_code(email, ver_code)

        result = await self._complete_login(user_id,
                                            login_submission,
                                            create_non_existent_users=True)
        return result

    # the login of msisdn verification code
    async def _do_ver_code_msisdn_login(
            self, login_submission: JsonDict) -> Dict[str, str]:
        msisdn = login_submission.get("msisdn", None)
        if msisdn is None:
            raise LoginError(410,
                             "msisdn field for ver_code_login is missing",
                             errcode=Codes.FORBIDDEN)
        # verify email and send to email
        user_id = await self.hs.get_datastore().get_user_id_by_threepid(
            "msisdn", msisdn)
        if user_id is None:
            raise SynapseError(400, "msisdn not bind",
                               Codes.TEMPORARY_NOT_BIND_MSISDN)

        ver_code = login_submission.get("ver_code", None)
        if ver_code is None:
            raise LoginError(411,
                             "ver_code field for ver_code_login is missing",
                             errcode=Codes.FORBIDDEN)

        # ver_code_service_host = "192.168.0.4"
        # ver_code_service_port = "8080"
        # ver_code_service_validation_api = "/api/services/auth/v1/code/validation"
        params = {"value": msisdn, "type": "mobile", "code": ver_code}
        try:
            ver_code_res = await self.http_client.post_json_get_json(
                self.hs.config.auth_baseurl +
                self.hs.config.auth_code_validation,
                params,
            )
            logger.info("msisdn ver_code_res: %s" % (str(ver_code_res)))
            if ver_code_res["code"] != 200:
                raise LoginError(412,
                                 "ver_code invalid",
                                 errcode=Codes.FORBIDDEN)
        except HttpResponseException as e:
            logger.info("Proxied validation vercode failed: %r", e)
            raise e.to_synapse_error()
        except RequestTimedOutError:
            raise SynapseError(
                500,
                "Timed out contacting extral server:ver_code_send_service")

        result = await self._complete_login(user_id,
                                            login_submission,
                                            create_non_existent_users=True)
        return result

    # the login of oauth2 e.g :alipay  weixin
    async def _do_oauth2_login(self,
                               login_submission: JsonDict) -> Dict[str, str]:
        logger.info("-----_do_oauth2_login-------login_submission:%s" %
                    (str(login_submission)))

        login_type = login_submission.get("type", None)
        auth_code = login_submission.get("auth_code", None)
        if auth_code is None:
            raise LoginError(410,
                             "auth_code field for oauth2 login is missing",
                             errcode=Codes.FORBIDDEN)
        app_type = 'weixin'
        _auth_provider_id = 'weixin'
        if login_type == LoginRestServlet.OAUTH2_ALIPAY_TYPE:
            _auth_provider_id = 'alipay'
            app_type = 'zhifubao'

        # Desttop handler,only weixin
        device_type = ''
        if login_type == LoginRestServlet.OAUTH2_WEIXIN_TYPE:
            device_type = login_submission.get('device_type', None)
            if device_type:
                app_type = 'weixin-' + device_type

        # call get_openid from authserver
        remote_user = await self.get_openid_by_code(app_type, auth_code)
        remote_user_id = remote_user['obj']
        if remote_user_id is None:
            raise LoginError(414,
                             "auth_code invalid",
                             errcode=Codes.INVALID_AUTH_CODE)

        # verify bind_type and openid
        user_id = await self.hs.get_datastore().get_user_by_external_id(
            _auth_provider_id, remote_user_id)
        if user_id is None:
            # openid set into cache,4 min
            self._cache[auth_code] = remote_user_id
            raise SynapseError(400, "oppenid not bind", Codes.OPENID_NOT_BIND)

        result = await self._complete_login(user_id,
                                            login_submission,
                                            create_non_existent_users=True)
        return result

    async def get_openid_by_code(self, app_type: str, auth_code: str):
        params = {
            "type": app_type,
            "value": auth_code,
        }
        logger.info("------------param:%s" % (str(params)))
        try:
            result = await self.http_client.post_json_get_json(
                self.hs.config.auth_baseurl + self.hs.config.auth_get_vercode,
                params,
            )
            logger.info("result: %s" % (str(result)))
            if result["code"] != 200:
                raise SynapseError(500, result["message"])

        except HttpResponseException as e:
            logger.info("Proxied get openid failed: %r", e)
            raise e.to_synapse_error()
        except RequestTimedOutError:
            raise SynapseError(500,
                               "Timed out contacting extral server:getopenid")
        return result

    # the login of sso ldap
    async def _do_sso_ldap_login(self,
                                 login_submission: JsonDict) -> Dict[str, str]:
        login_type = login_submission.get("type", None)
        username = login_submission.get("user", None)
        if username is None:
            raise LoginError(410,
                             "user field for ldap login is missing",
                             errcode=Codes.FORBIDDEN)
        password = login_submission.get("password", None)
        # logger.debug("----------------------password:%s" % (password,))
        if password is None:
            raise LoginError(410,
                             "password field for ldap login is missing",
                             errcode=Codes.FORBIDDEN)

        if username.startswith("@"):
            qualified_user_id = username
        else:
            qualified_user_id = UserID(username, self.hs.hostname).to_string()
        # verify bind_type and openid
        user_id = await self.auth_handler.check_user_exists(qualified_user_id)
        logger.info("----------------------exists user_id:%s" % (user_id, ))
        if user_id is None:
            raise SynapseError(400, "user not exists", Codes.INVALID_USERNAME)

        params = {
            "account": username,
            "password": password,
        }
        try:
            ldap_ver_res = await self.http_client.post_json_get_json(
                self.hs.config.auth_baseurl +
                self.hs.config.auth_sso_ldap_validation,
                params,
            )
            logger.info("ldap verification result: %s" % (str(ldap_ver_res)))
            if ldap_ver_res["code"] != 200:
                raise SynapseError(500, ldap_ver_res["message"])
        except HttpResponseException as e:
            logger.info("Proxied ldap verification failed: %r", e)
            raise e.to_synapse_error()
        except RequestTimedOutError:
            raise SynapseError(
                500, "Timed out contacting extral server:ldap verification")

        result = await self._complete_login(user_id,
                                            login_submission,
                                            create_non_existent_users=True)
        return result

    async def _do_jwt_login(self,
                            login_submission: JsonDict) -> Dict[str, str]:
        token = login_submission.get("token", None)
        if token is None:
            raise LoginError(403,
                             "Token field for JWT is missing",
                             errcode=Codes.FORBIDDEN)

        import jwt

        try:
            payload = jwt.decode(
                token,
                self.jwt_secret,
                algorithms=[self.jwt_algorithm],
                issuer=self.jwt_issuer,
                audience=self.jwt_audiences,
            )
        except jwt.PyJWTError as e:
            # A JWT error occurred, return some info back to the client.
            raise LoginError(
                403,
                "JWT validation failed: %s" % (str(e), ),
                errcode=Codes.FORBIDDEN,
            )

        user = payload.get("sub", None)
        if user is None:
            raise LoginError(403, "Invalid JWT", errcode=Codes.FORBIDDEN)

        user_id = UserID(user, self.hs.hostname).to_string()
        result = await self._complete_login(user_id,
                                            login_submission,
                                            create_non_existent_users=True)
        return result
Ejemplo n.º 7
0
class LoginRestServlet(ClientV1RestServlet):
    PATTERNS = client_path_patterns("/login$")
    CAS_TYPE = "m.login.cas"
    SSO_TYPE = "m.login.sso"
    TOKEN_TYPE = "m.login.token"
    JWT_TYPE = "m.login.jwt"

    def __init__(self, hs):
        super(LoginRestServlet, self).__init__(hs)
        self.jwt_enabled = hs.config.jwt_enabled
        self.jwt_secret = hs.config.jwt_secret
        self.jwt_algorithm = hs.config.jwt_algorithm
        self.cas_enabled = hs.config.cas_enabled
        self.auth_handler = self.hs.get_auth_handler()
        self.device_handler = self.hs.get_device_handler()
        self.handlers = hs.get_handlers()
        self._well_known_builder = WellKnownBuilder(hs)

    def on_GET(self, request):
        flows = []
        if self.jwt_enabled:
            flows.append({"type": LoginRestServlet.JWT_TYPE})
        if self.cas_enabled:
            flows.append({"type": LoginRestServlet.SSO_TYPE})

            # we advertise CAS for backwards compat, though MSC1721 renamed it
            # to SSO.
            flows.append({"type": LoginRestServlet.CAS_TYPE})

            # While its valid for us to advertise this login type generally,
            # synapse currently only gives out these tokens as part of the
            # CAS login flow.
            # Generally we don't want to advertise login flows that clients
            # don't know how to implement, since they (currently) will always
            # fall back to the fallback API if they don't understand one of the
            # login flow types returned.
            flows.append({"type": LoginRestServlet.TOKEN_TYPE})

        flows.extend(({
            "type": t
        } for t in self.auth_handler.get_supported_login_types()))

        return (200, {"flows": flows})

    def on_OPTIONS(self, request):
        return (200, {})

    @defer.inlineCallbacks
    def on_POST(self, request):
        login_submission = parse_json_object_from_request(request)
        try:
            if self.jwt_enabled and (login_submission["type"]
                                     == LoginRestServlet.JWT_TYPE):
                result = yield self.do_jwt_login(login_submission)
            elif login_submission["type"] == LoginRestServlet.TOKEN_TYPE:
                result = yield self.do_token_login(login_submission)
            else:
                result = yield self._do_other_login(login_submission)
        except KeyError:
            raise SynapseError(400, "Missing JSON keys.")

        well_known_data = self._well_known_builder.get_well_known()
        if well_known_data:
            result["well_known"] = well_known_data
        defer.returnValue((200, result))

    @defer.inlineCallbacks
    def _do_other_login(self, login_submission):
        """Handle non-token/saml/jwt logins

        Args:
            login_submission:

        Returns:
            dict: 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")

            if medium == 'email':
                # For emails, transform the address to lowercase.
                # We store all email addreses as lowercase in the DB.
                # (See add_threepid in synapse/handlers/auth.py)
                address = address.lower()
            user_id = yield self.hs.get_datastore().get_user_id_by_threepid(
                medium,
                address,
            )
            if not user_id:
                logger.warn(
                    "unknown 3pid identifier medium %s, address %r",
                    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")

        auth_handler = self.auth_handler
        canonical_user_id, callback = yield auth_handler.validate_login(
            identifier["user"],
            login_submission,
        )

        device_id = yield self._register_device(
            canonical_user_id,
            login_submission,
        )
        access_token = yield auth_handler.get_access_token_for_user_id(
            canonical_user_id,
            device_id,
        )

        result = {
            "user_id": canonical_user_id,
            "access_token": access_token,
            "home_server": self.hs.hostname,
            "device_id": device_id,
        }

        if callback is not None:
            yield callback(result)

        defer.returnValue(result)

    @defer.inlineCallbacks
    def do_token_login(self, login_submission):
        token = login_submission['token']
        auth_handler = self.auth_handler
        user_id = (yield auth_handler.
                   validate_short_term_login_token_and_get_user_id(token))
        device_id = yield self._register_device(user_id, login_submission)
        access_token = yield auth_handler.get_access_token_for_user_id(
            user_id,
            device_id,
        )
        result = {
            "user_id": user_id,  # may have changed
            "access_token": access_token,
            "home_server": self.hs.hostname,
            "device_id": device_id,
        }

        defer.returnValue(result)

    @defer.inlineCallbacks
    def do_jwt_login(self, login_submission):
        token = login_submission.get("token", None)
        if token is None:
            raise LoginError(401,
                             "Token field for JWT is missing",
                             errcode=Codes.UNAUTHORIZED)

        import jwt
        from jwt.exceptions import InvalidTokenError

        try:
            payload = jwt.decode(token,
                                 self.jwt_secret,
                                 algorithms=[self.jwt_algorithm])
        except jwt.ExpiredSignatureError:
            raise LoginError(401, "JWT expired", errcode=Codes.UNAUTHORIZED)
        except InvalidTokenError:
            raise LoginError(401, "Invalid JWT", errcode=Codes.UNAUTHORIZED)

        user = payload.get("sub", None)
        if user is None:
            raise LoginError(401, "Invalid JWT", errcode=Codes.UNAUTHORIZED)

        user_id = UserID(user, self.hs.hostname).to_string()
        auth_handler = self.auth_handler
        registered_user_id = yield auth_handler.check_user_exists(user_id)
        if registered_user_id:
            device_id = yield self._register_device(registered_user_id,
                                                    login_submission)
            access_token = yield auth_handler.get_access_token_for_user_id(
                registered_user_id,
                device_id,
            )

            result = {
                "user_id": registered_user_id,
                "access_token": access_token,
                "home_server": self.hs.hostname,
            }
        else:
            # TODO: we should probably check that the register isn't going
            # to fonx/change our user_id before registering the device
            device_id = yield self._register_device(user_id, login_submission)
            user_id, access_token = (
                yield
                self.handlers.registration_handler.register(localpart=user))
            result = {
                "user_id": user_id,  # may have changed
                "access_token": access_token,
                "home_server": self.hs.hostname,
            }

        defer.returnValue(result)

    def _register_device(self, user_id, login_submission):
        """Register a device for a user.

        This is called after the user's credentials have been validated, but
        before the access token has been issued.

        Args:
            (str) user_id: full canonical @user:id
            (object) login_submission: dictionary supplied to /login call, from
               which we pull device_id and initial_device_name
        Returns:
            defer.Deferred: (str) device_id
        """
        device_id = login_submission.get("device_id")
        initial_display_name = login_submission.get(
            "initial_device_display_name")
        return self.device_handler.check_device_registered(
            user_id, device_id, initial_display_name)
Ejemplo n.º 8
0
class LoginRestServlet(RestServlet):
    PATTERNS = client_patterns("/login$", v1=True)
    CAS_TYPE = "m.login.cas"
    SSO_TYPE = "m.login.sso"
    TOKEN_TYPE = "m.login.token"
    JWT_TYPE = "org.matrix.login.jwt"
    JWT_TYPE_DEPRECATED = "m.login.jwt"
    APPSERVICE_TYPE = "uk.half-shot.msc2778.login.application_service"

    def __init__(self, hs):
        super().__init__()
        self.hs = hs

        # JWT configuration variables.
        self.jwt_enabled = hs.config.jwt_enabled
        self.jwt_secret = hs.config.jwt_secret
        self.jwt_algorithm = hs.config.jwt_algorithm
        self.jwt_issuer = hs.config.jwt_issuer
        self.jwt_audiences = hs.config.jwt_audiences

        # SSO configuration.
        self.saml2_enabled = hs.config.saml2_enabled
        self.cas_enabled = hs.config.cas_enabled
        self.oidc_enabled = hs.config.oidc_enabled

        self.auth = hs.get_auth()

        self.auth_handler = self.hs.get_auth_handler()
        self.registration_handler = hs.get_registration_handler()
        self._well_known_builder = WellKnownBuilder(hs)
        self._address_ratelimiter = Ratelimiter(
            clock=hs.get_clock(),
            rate_hz=self.hs.config.rc_login_address.per_second,
            burst_count=self.hs.config.rc_login_address.burst_count,
        )
        self._account_ratelimiter = Ratelimiter(
            clock=hs.get_clock(),
            rate_hz=self.hs.config.rc_login_account.per_second,
            burst_count=self.hs.config.rc_login_account.burst_count,
        )
        self._failed_attempts_ratelimiter = Ratelimiter(
            clock=hs.get_clock(),
            rate_hz=self.hs.config.rc_login_failed_attempts.per_second,
            burst_count=self.hs.config.rc_login_failed_attempts.burst_count,
        )

    def on_GET(self, request: SynapseRequest):
        flows = []
        if self.jwt_enabled:
            flows.append({"type": LoginRestServlet.JWT_TYPE})
            flows.append({"type": LoginRestServlet.JWT_TYPE_DEPRECATED})

        if self.cas_enabled:
            # we advertise CAS for backwards compat, though MSC1721 renamed it
            # to SSO.
            flows.append({"type": LoginRestServlet.CAS_TYPE})

        if self.cas_enabled or self.saml2_enabled or self.oidc_enabled:
            flows.append({"type": LoginRestServlet.SSO_TYPE})
            # While its valid for us to advertise this login type generally,
            # synapse currently only gives out these tokens as part of the
            # SSO login flow.
            # Generally we don't want to advertise login flows that clients
            # don't know how to implement, since they (currently) will always
            # fall back to the fallback API if they don't understand one of the
            # login flow types returned.
            flows.append({"type": LoginRestServlet.TOKEN_TYPE})

        flows.extend(({
            "type": t
        } for t in self.auth_handler.get_supported_login_types()))

        flows.append({"type": LoginRestServlet.APPSERVICE_TYPE})

        return 200, {"flows": flows}

    async def on_POST(self, request: SynapseRequest):
        self._address_ratelimiter.ratelimit(request.getClientIP())

        login_submission = parse_json_object_from_request(request)

        try:
            if login_submission["type"] == LoginRestServlet.APPSERVICE_TYPE:
                appservice = self.auth.get_appservice_by_req(request)
                result = await self._do_appservice_login(
                    login_submission, appservice)
            elif self.jwt_enabled and (
                    login_submission["type"] == LoginRestServlet.JWT_TYPE
                    or login_submission["type"]
                    == LoginRestServlet.JWT_TYPE_DEPRECATED):
                result = await self._do_jwt_login(login_submission)
            elif login_submission["type"] == LoginRestServlet.TOKEN_TYPE:
                result = await self._do_token_login(login_submission)
            else:
                result = await self._do_other_login(login_submission)
        except KeyError:
            raise SynapseError(400, "Missing JSON keys.")

        well_known_data = self._well_known_builder.get_well_known()
        if well_known_data:
            result["well_known"] = well_known_data
        return 200, result

    def _get_qualified_user_id(self, identifier):
        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("@"):
            return identifier["user"]
        else:
            return UserID(identifier["user"], self.hs.hostname).to_string()

    async def _do_appservice_login(self, login_submission: JsonDict,
                                   appservice: ApplicationService):
        logger.info(
            "Got appservice login request with identifier: %r",
            login_submission.get("identifier"),
        )

        identifier = convert_client_dict_legacy_fields_to_identifier(
            login_submission)
        qualified_user_id = self._get_qualified_user_id(identifier)

        if not appservice.is_interested_in_user(qualified_user_id):
            raise LoginError(403,
                             "Invalid access_token",
                             errcode=Codes.FORBIDDEN)

        return await self._complete_login(qualified_user_id, login_submission)

    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"),
        )
        identifier = convert_client_dict_legacy_fields_to_identifier(
            login_submission)

        # convert phone type identifiers to generic threepids
        if identifier["type"] == "m.id.phone":
            identifier = login_id_phone_to_thirdparty(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.
        qualified_user_id = self._get_qualified_user_id(identifier)

        # 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 _complete_login(
        self,
        user_id: str,
        login_submission: JsonDict,
        callback: Optional[Callable[[Dict[str, str]], Awaitable[None]]] = None,
        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
        all successful logins.

        Applies the ratelimiting for successful login attempts against an
        account.

        Args:
            user_id: ID of the user to register.
            login_submission: Dictionary of login information.
            callback: Callback function to run after login.
            create_non_existent_users: Whether to create the user if they don't
                exist. Defaults to False.

        Returns:
            result: Dictionary of account information after successful login.
        """

        # Before we actually log them in we check if they've already logged in
        # too often. This happens here rather than before as we don't
        # necessarily know the user before now.
        self._account_ratelimiter.ratelimit(user_id.lower())

        if create_non_existent_users:
            canonical_uid = await self.auth_handler.check_user_exists(user_id)
            if not canonical_uid:
                canonical_uid = await self.registration_handler.register_user(
                    localpart=UserID.from_string(user_id).localpart)
            user_id = canonical_uid

        device_id = login_submission.get("device_id")
        initial_display_name = login_submission.get(
            "initial_device_display_name")
        device_id, access_token = await self.registration_handler.register_device(
            user_id, device_id, initial_display_name)

        result = {
            "user_id": user_id,
            "access_token": access_token,
            "home_server": self.hs.hostname,
            "device_id": device_id,
        }

        if callback is not None:
            await callback(result)

        return result

    async def _do_token_login(self,
                              login_submission: JsonDict) -> Dict[str, str]:
        """
        Handle the final stage of SSO login.

        Args:
             login_submission: The JSON request body.

        Returns:
            The body of the JSON response.
        """
        token = login_submission["token"]
        auth_handler = self.auth_handler
        user_id = await auth_handler.validate_short_term_login_token_and_get_user_id(
            token)

        return await self._complete_login(
            user_id, login_submission, self.auth_handler._sso_login_callback)

    async def _do_jwt_login(self,
                            login_submission: JsonDict) -> Dict[str, str]:
        token = login_submission.get("token", None)
        if token is None:
            raise LoginError(403,
                             "Token field for JWT is missing",
                             errcode=Codes.FORBIDDEN)

        import jwt

        try:
            payload = jwt.decode(
                token,
                self.jwt_secret,
                algorithms=[self.jwt_algorithm],
                issuer=self.jwt_issuer,
                audience=self.jwt_audiences,
            )
        except jwt.PyJWTError as e:
            # A JWT error occurred, return some info back to the client.
            raise LoginError(
                403,
                "JWT validation failed: %s" % (str(e), ),
                errcode=Codes.FORBIDDEN,
            )

        user = payload.get("sub", None)
        if user is None:
            raise LoginError(403, "Invalid JWT", errcode=Codes.FORBIDDEN)

        user_id = UserID(user, self.hs.hostname).to_string()
        result = await self._complete_login(user_id,
                                            login_submission,
                                            create_non_existent_users=True)
        return result