コード例 #1
0
ファイル: auth.py プロジェクト: tuchang/synapse
    def validate_login(self, username, login_submission):
        """Authenticates the user for the /login API

        Also used by the user-interactive auth flow to validate
        m.login.password auth types.

        Args:
            username (str): username supplied by the user
            login_submission (dict): the whole of the login submission
                (including 'type' and other relevant fields)
        Returns:
            Deferred[str, func]: canonical user id, and optional callback
                to be called once the access token and device id are issued
        Raises:
            StoreError if there was a problem accessing the database
            SynapseError if there was a problem with the request
            LoginError if there was an authentication problem.
            LimitExceededError if the ratelimiter's login requests count for this
                user is too high too proceed.
        """

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

        self.ratelimit_login_per_account(qualified_user_id)

        login_type = login_submission.get("type")
        known_login_type = False

        # special case to check for "password" for the check_password interface
        # for the auth providers
        password = login_submission.get("password")

        if login_type == LoginType.PASSWORD:
            if not self._password_enabled:
                raise SynapseError(400, "Password login has been disabled.")
            if not password:
                raise SynapseError(400, "Missing parameter: password")

        for provider in self.password_providers:
            if hasattr(provider, "check_password") and login_type == LoginType.PASSWORD:
                known_login_type = True
                is_valid = yield provider.check_password(qualified_user_id, password)
                if is_valid:
                    defer.returnValue((qualified_user_id, None))

            if not hasattr(provider, "get_supported_login_types") or not hasattr(
                provider, "check_auth"
            ):
                # this password provider doesn't understand custom login types
                continue

            supported_login_types = provider.get_supported_login_types()
            if login_type not in supported_login_types:
                # this password provider doesn't understand this login type
                continue

            known_login_type = True
            login_fields = supported_login_types[login_type]

            missing_fields = []
            login_dict = {}
            for f in login_fields:
                if f not in login_submission:
                    missing_fields.append(f)
                else:
                    login_dict[f] = login_submission[f]
            if missing_fields:
                raise SynapseError(
                    400,
                    "Missing parameters for login type %s: %s"
                    % (login_type, missing_fields),
                )

            result = yield provider.check_auth(username, login_type, login_dict)
            if result:
                if isinstance(result, str):
                    result = (result, None)
                defer.returnValue(result)

        if login_type == LoginType.PASSWORD and self.hs.config.password_localdb_enabled:
            known_login_type = True

            canonical_user_id = yield self._check_local_password(
                qualified_user_id, password
            )

            if canonical_user_id:
                defer.returnValue((canonical_user_id, None))

        if not known_login_type:
            raise SynapseError(400, "Unknown login type %s" % login_type)

        # unknown username or invalid password.
        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=True,
        )

        # We raise a 403 here, but note that if we're doing user-interactive
        # login, it turns all LoginErrors into a 401 anyway.
        raise LoginError(403, "Invalid password", errcode=Codes.FORBIDDEN)
コード例 #2
0
    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
コード例 #3
0
    def check_passwd(self, user_id, password):
        """ Authenticate a user against an LDAP Server
            and register an account if none exists.

            Returns:
                True if authentication against LDAP was successful
        """
        if user_id.startswith("@"):
            localpart = user_id.split(":", 1)[0][1:]
        else:
            localpart = user_id
            user_id = UserID(localpart, self.account_handler.hs.hostname).to_string()

        now = time.time()
        if localpart in self.bad_login_attemps.keys():
            if self.bad_login_attemps[localpart]['count'] >= self.ldap_alp['attemps']:
                unlock_time = self.bad_login_attemps[localpart]['ts'] + \
                    self.ldap_alp['locktime_s']
                if now <= unlock_time:
                    logger.error(
                        'User %s is locked by account lockout policy. '
                        'This login attemp will fail. '
                        'Seconds to unlock: %d' %
                        (user_id, unlock_time - now)
                    )
                    defer.returnValue(False)

        try:
            server = ldap3.Server(self.ldap_uri, get_info=None)
            logger.debug(
                'LDAP connection with %s',
                self.ldap_uri
            )

            if self.ldap_mode == 'simple':
                bind_dn = "{prop}={value},{base}".format(
                    prop=self.ldap_attributes['uid'],
                    value=localpart,
                    base=self.ldap_base
                )
                result, conn = yield self._ldap_simple_bind(
                    server=server, bind_dn=bind_dn, password=password
                )
                logger.debug(
                    'LDAP authentication method simple bind returned: '
                    '%s (conn: %s)',
                    result,
                    conn
                )
                if not result:
                    if self.ldap_alp_exists:
                        if localpart in self.bad_login_attemps.keys():
                            self.bad_login_attemps[localpart]['count'] += 1
                            self.bad_login_attemps[localpart]['ts'] = now
                        else:
                            self.bad_login_attemps[localpart] = {
                                'count': 1,
                                'ts': now
                            }
                    defer.returnValue(False)
            elif self.ldap_mode == 'search':
                result, conn = yield self._ldap_authenticated_search(
                    server=server, localpart=localpart, password=password
                )
                logger.debug(
                    'LDAP auth method authenticated search returned: '
                    '%s ',
                    result
                )
                if not result:
                    if self.ldap_alp_exists:
                        if localpart in self.bad_login_attemps.keys():
                            self.bad_login_attemps[localpart]['count'] += 1
                            self.bad_login_attemps[localpart]['ts'] = now
                        else:
                            self.bad_login_attemps[localpart] = {
                                'count': 1,
                                'ts': now
                            }
                    defer.returnValue(False)
            else:
                raise RuntimeError(
                    'Invalid LDAP mode specified: {%s}' %
                    self.ldap_mode
                )
            if not conn:
                logger.error(
                    'Authentication method yielded no LDAP connection, '
                    'aborting!'
                )
                defer.returnValue(False)

            query = '({prop}={value})'.format(
                prop=self.ldap_attributes['uid'],
                value=localpart
            )

            if self.ldap_mode == 'search' and self.ldap_filter:
                query = '(&{filter}{user_filter})'.format(
                    filter=query,
                    user_filter=self.ldap_filter
                )
            logger.debug(
                'LDAP search filter: %s',
                query
            )

            yield threads.deferToThread(
                conn.search,
                search_base=self.ldap_base,
                search_filter=query,
                attributes=self.ldap_attributes.values()
            )

            responses = [
                response
                for response
                in conn.response
                if response['type'] == 'searchResEntry'
            ]

            if len(responses) == 1:
                attrs = responses[0]['attributes']
                try:
                    name = attrs[self.ldap_attributes['name']][0]
                except Exception:
                    name = None

                store = self.account_handler.hs.get_profile_handler().store
                users = yield store.get_users_by_id_case_insensitive(user_id)
                if not users:
                    # Create account if not exists
                    logger.info(
                        'FIRST login for user %s' %
                        user_id
                    )
                    user_id, access_token = (
                        yield self.account_handler.register(localpart=localpart)
                    )

                if name is not None:
                    # Update user Display Name
                    store.set_profile_displayname(localpart, name)
                    profile = yield store.get_profileinfo(localpart)
                    user_dir_handler = self.account_handler.hs.get_user_directory_handler()
                    yield user_dir_handler.handle_local_profile_change(
                        user_id, profile
                    )

                if 'mail' in self.ldap_attributes:
                    for mail in attrs[self.ldap_attributes['mail']]:
                        # Update user email
                        validated_at = self.account_handler.hs.get_clock().time_msec()
                        user_id_by_threepid = yield store.get_user_id_by_threepid(
                            'email',
                            mail
                        )
                        # add email only if not exists
                        if not user_id_by_threepid:
                            store.user_add_threepid(
                                user_id,
                                'email',
                                mail,
                                validated_at,
                                validated_at
                            )
                        elif not user_id_by_threepid.lower() == user_id.lower():
                            logger.error(
                                'Auth user %s with %s email but user %s'
                                'already have same email' % (
                                    user_id,
                                    mail,
                                    user_id_by_threepid
                                )
                            )

                if 'msisdn' in self.ldap_attributes:
                    for msisdn in attrs[self.ldap_attributes['msisdn']]:
                        # Update user msisdn
                        validated_at = self.account_handler.hs.get_clock().time_msec()
                        user_id_by_threepid = yield store.get_user_id_by_threepid(
                            'msisdn',
                            msisdn
                        )
                        # add msisdn only if not exists
                        if not user_id_by_threepid:
                            store.user_add_threepid(
                                user_id,
                                'msisdn',
                                msisdn,
                                validated_at,
                                validated_at
                            )
                        elif not user_id_by_threepid.lower() == user_id.lower():
                            logger.error(
                                'Auth user %s with %s msisdn but user %s'
                                'already have same msisdn' % (
                                    user_id,
                                    msisdn,
                                    user_id_by_threepid
                                )
                            )

                logger.info(
                    'Auth based on LDAP data was successful: '
                    '%s: %s (%s)',
                    user_id, localpart, name
                )
                if localpart in self.bad_login_attemps:
                    del self.bad_login_attemps[localpart]
                defer.returnValue(True)
            else:
                if len(responses) == 0:
                    logger.warning('LDAP auth failed, no result.')
                else:
                    logger.warning(
                        'LDAP auth failed, too many results (%s)',
                        len(responses)
                    )
                defer.returnValue(False)

            defer.returnValue(False)

        except ldap3.core.exceptions.LDAPException as e:
            logger.warning('Error during ldap authentication: %s', e)
            defer.returnValue(False)