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)
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
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)