diff --git a/ChangeLog.d/feature/ZBXNEXT-2289 b/ChangeLog.d/feature/ZBXNEXT-2289
new file mode 100644
index 00000000000..e78084f8714
--- /dev/null
+++ b/ChangeLog.d/feature/ZBXNEXT-2289
@@ -0,0 +1 @@
+A.F.I..... [ZBXNEXT-2289] implemented multiple LDAP sources for user authentication (gcalenko, vmurzins)
diff --git a/create/src/schema.tmpl b/create/src/schema.tmpl
index ef300d065fa..a9c7bae8753 100644
--- a/create/src/schema.tmpl
+++ b/create/src/schema.tmpl
@@ -341,7 +341,9 @@ FIELD		|name		|t_varchar(64)	|''	|NOT NULL	|0
 FIELD		|gui_access	|t_integer	|'0'	|NOT NULL	|0
 FIELD		|users_status	|t_integer	|'0'	|NOT NULL	|0
 FIELD		|debug_mode	|t_integer	|'0'	|NOT NULL	|0
+FIELD		|userdirectoryid	|t_id	|NULL	|NULL	|0 |2|userdirectory	|	|RESTRICT
 UNIQUE		|1		|name
+INDEX		|2	|userdirectoryid
 
 TABLE|users_groups|id|ZBX_DATA
 FIELD		|id		|t_id		|	|NOT NULL	|0
@@ -483,12 +485,6 @@ FIELD		|work_period	|t_varchar(255)	|'1-5,09:00-18:00'|NOT NULL|0
 FIELD		|alert_usrgrpid	|t_id		|	|NULL		|0			|1|usrgrp	|usrgrpid	|RESTRICT
 FIELD		|default_theme	|t_varchar(128)	|'blue-theme'|NOT NULL	|ZBX_NODATA
 FIELD		|authentication_type|t_integer	|'0'	|NOT NULL	|ZBX_NODATA
-FIELD		|ldap_host	|t_varchar(255)	|''	|NOT NULL	|ZBX_NODATA
-FIELD		|ldap_port	|t_integer	|389	|NOT NULL	|ZBX_NODATA
-FIELD		|ldap_base_dn	|t_varchar(255)	|''	|NOT NULL	|ZBX_NODATA
-FIELD		|ldap_bind_dn	|t_varchar(255)	|''	|NOT NULL	|ZBX_NODATA
-FIELD		|ldap_bind_password|t_varchar(128)|''	|NOT NULL	|ZBX_NODATA
-FIELD		|ldap_search_attribute|t_varchar(128)|''|NOT NULL	|ZBX_NODATA
 FIELD		|discovery_groupid|t_id		|	|NOT NULL	|ZBX_PROXY		|2|hstgrp	|groupid	|RESTRICT
 FIELD		|max_in_table	|t_integer	|'50'	|NOT NULL	|ZBX_NODATA
 FIELD		|search_limit	|t_integer	|'1000'	|NOT NULL	|ZBX_NODATA
@@ -593,8 +589,10 @@ FIELD		|geomaps_tile_provider|t_varchar(255)	|''	|NOT NULL	|0
 FIELD		|geomaps_tile_url	|t_varchar(1024)|''	|NOT NULL	|ZBX_NODATA
 FIELD		|geomaps_max_zoom	|t_integer	|'0'	|NOT NULL	|ZBX_NODATA
 FIELD		|geomaps_attribution|t_varchar(1024)|''	|NOT NULL	|ZBX_NODATA
+FIELD		|ldap_userdirectoryid	|t_id	|NULL |NULL	|0		|3|userdirectory	|userdirectoryid|RESTRICT
 INDEX		|1		|alert_usrgrpid
 INDEX		|2		|discovery_groupid
+INDEX		|3		|ldap_userdirectoryid
 
 TABLE|triggers|triggerid|ZBX_TEMPLATE
 FIELD		|triggerid	|t_id		|	|NOT NULL	|0
@@ -1932,8 +1930,21 @@ FIELD		|operator		|t_integer	|'0'	|NOT NULL	|0
 FIELD		|value			|t_varchar(255)	|''	|NOT NULL	|0
 INDEX		|1			|slaid
 
+TABLE|userdirectory|userdirectoryid
+FIELD		|userdirectoryid	|t_id			|		|NOT NULL	|0
+FIELD		|name				|t_varchar(128)	|''		|NOT NULL	|0
+FIELD		|description		|t_shorttext	|''		|NOT NULL	|0
+FIELD		|host				|t_varchar(255)	|''		|NOT NULL	|0
+FIELD		|port				|t_integer		|'389'	|NOT NULL	|0
+FIELD		|base_dn			|t_varchar(255)	|''		|NOT NULL	|0
+FIELD		|bind_dn			|t_varchar(255)	|''		|NOT NULL	|0
+FIELD		|bind_password		|t_varchar(128)	|''		|NOT NULL	|0
+FIELD		|search_attribute	|t_varchar(128)	|''		|NOT NULL	|0
+FIELD		|start_tls			|t_integer		|'0'	|NOT NULL	|0
+FIELD		|search_filter		|t_varchar(255)	|''		|NOT NULL	|0
+
 TABLE|dbversion|dbversionid|
 FIELD		|dbversionid	|t_id		|	|NOT NULL	|0
 FIELD		|mandatory	|t_integer	|'0'	|NOT NULL	|
 FIELD		|optional	|t_integer	|'0'	|NOT NULL	|
-ROW		|1		|6000000	|6000000
+ROW		|1		|6000015	|6000015
diff --git a/sass/stylesheets/sass/components/_z-select.scss b/sass/stylesheets/sass/components/_z-select.scss
index 9989a55e908..db94f2dc46f 100644
--- a/sass/stylesheets/sass/components/_z-select.scss
+++ b/sass/stylesheets/sass/components/_z-select.scss
@@ -6,6 +6,10 @@ z-select,
 		outline: none;
 	}
 
+	ul > li.default-option {
+		font-style: italic;
+	}
+
 	button.focusable {
 		position: relative;
 		width: 100%;
diff --git a/src/libs/zbxdbupgrade/dbupgrade_6000.c b/src/libs/zbxdbupgrade/dbupgrade_6000.c
index 02e39f9687b..5b55293e110 100644
--- a/src/libs/zbxdbupgrade/dbupgrade_6000.c
+++ b/src/libs/zbxdbupgrade/dbupgrade_6000.c
@@ -18,6 +18,7 @@
 **/
 
 #include "common.h"
+#include "db.h"
 #include "dbupgrade.h"
 
 extern unsigned char	program_type;
@@ -33,6 +34,146 @@ static int	DBpatch_6000000(void)
 	return SUCCEED;
 }
 
+static int	DBpatch_6000001(void)
+{
+	const ZBX_TABLE	table =
+		{"userdirectory", "userdirectoryid", 0,
+			{
+				{"userdirectoryid", NULL, NULL, NULL, 0, ZBX_TYPE_ID, ZBX_NOTNULL, 0},
+				{"name", "", NULL, NULL, 128, ZBX_TYPE_CHAR, ZBX_NOTNULL, 0},
+				{"description", "", NULL, NULL, 255, ZBX_TYPE_SHORTTEXT, ZBX_NOTNULL, 0},
+				{"host", "", NULL, NULL, 255, ZBX_TYPE_CHAR, ZBX_NOTNULL, 0},
+				{"port", "389", NULL, NULL, 0, ZBX_TYPE_INT, ZBX_NOTNULL, 0},
+				{"base_dn", "", NULL, NULL, 255, ZBX_TYPE_CHAR, ZBX_NOTNULL, 0},
+				{"bind_dn", "", NULL, NULL, 255, ZBX_TYPE_CHAR, ZBX_NOTNULL, 0},
+				{"bind_password", "", NULL, NULL, 128, ZBX_TYPE_CHAR, ZBX_NOTNULL, 0},
+				{"search_attribute", "", NULL, NULL, 128, ZBX_TYPE_CHAR, ZBX_NOTNULL, 0},
+				{"start_tls", "0", NULL, NULL, 0, ZBX_TYPE_INT, ZBX_NOTNULL, 0},
+				{"search_filter", "", NULL, NULL, 255, ZBX_TYPE_CHAR, ZBX_NOTNULL, 0},
+				{0}
+			},
+			NULL
+		};
+
+	return DBcreate_table(&table);
+}
+
+static int	DBpatch_6000002(void)
+{
+	const ZBX_FIELD	field = {"ldap_userdirectoryid", NULL, NULL, NULL, 0, ZBX_TYPE_ID, 0, 0};
+
+	return DBadd_field("config", &field);
+}
+
+static int	DBpatch_6000003(void)
+{
+	const ZBX_FIELD	field = {"ldap_userdirectoryid", NULL, "userdirectory", "userdirectoryid", 0, ZBX_TYPE_ID, 0, 0};
+
+	return DBadd_foreign_key("config", 3, &field);
+}
+
+static int	DBpatch_6000004(void)
+{
+	const ZBX_FIELD	field = {"userdirectoryid", NULL, NULL, NULL, 0, ZBX_TYPE_ID, 0, 0};
+
+	return DBadd_field("usrgrp", &field);
+}
+
+static int	DBpatch_6000005(void)
+{
+	int		rc = ZBX_DB_OK;
+	DB_RESULT	result;
+	DB_ROW		row;
+
+	if (NULL == (result = DBselect("select ldap_host,ldap_port,ldap_base_dn,ldap_bind_dn,"
+			"ldap_bind_password,ldap_search_attribute"
+			" from config where ldap_configured=1")))
+	{
+		return FAIL;
+	}
+
+	if (NULL != (row = DBfetch(result)))
+	{
+		char	*base_dn_esc, *bind_dn_esc, *password_esc, *search_esc;
+
+		base_dn_esc = DBdyn_escape_string(row[2]);
+		bind_dn_esc = DBdyn_escape_string(row[3]);
+		password_esc = DBdyn_escape_string(row[4]);
+		search_esc = DBdyn_escape_string(row[5]);
+
+		rc = DBexecute("insert into userdirectory (userdirectoryid,name,description,host,port,"
+				"base_dn,bind_dn,bind_password,search_attribute,start_tls) values "
+				"(1,'Default LDAP server','','%s',%s,'%s','%s','%s','%s',%d)",
+				row[0], row[1], base_dn_esc, bind_dn_esc, password_esc, search_esc, 0);
+
+		zbx_free(search_esc);
+		zbx_free(password_esc);
+		zbx_free(bind_dn_esc);
+		zbx_free(base_dn_esc);
+	}
+
+	DBfree_result(result);
+
+	if (ZBX_DB_OK > rc)
+		return FAIL;
+
+	return SUCCEED;
+}
+
+static int	DBpatch_6000006(void)
+{
+	if (ZBX_DB_OK > DBexecute("update config set ldap_userdirectoryid=1 where ldap_configured=1"))
+		return FAIL;
+
+	return SUCCEED;
+}
+
+static int	DBpatch_6000007(void)
+{
+	return DBdrop_field("config", "ldap_host");
+}
+
+static int	DBpatch_6000008(void)
+{
+	return DBdrop_field("config", "ldap_port");
+}
+
+static int	DBpatch_6000009(void)
+{
+	return DBdrop_field("config", "ldap_base_dn");
+}
+
+static int	DBpatch_6000010(void)
+{
+	return DBdrop_field("config", "ldap_bind_dn");
+}
+
+static int	DBpatch_6000011(void)
+{
+	return DBdrop_field("config", "ldap_bind_password");
+}
+
+static int	DBpatch_6000012(void)
+{
+	return DBdrop_field("config", "ldap_search_attribute");
+}
+
+static int	DBpatch_6000013(void)
+{
+	return DBcreate_index("config", "config_3", "ldap_userdirectoryid", 0);
+}
+
+static int	DBpatch_6000014(void)
+{
+	const ZBX_FIELD	field = {"userdirectoryid", NULL, "userdirectory", "userdirectoryid", 0, ZBX_TYPE_ID, 0, 0};
+
+	return DBadd_foreign_key("usrgrp", 2, &field);
+}
+
+static int	DBpatch_6000015(void)
+{
+	return DBcreate_index("usrgrp", "usrgrp_2", "userdirectoryid", 0);
+}
 #endif
 
 DBPATCH_START(6000)
@@ -40,5 +181,20 @@ DBPATCH_START(6000)
 /* version, duplicates flag, mandatory flag */
 
 DBPATCH_ADD(6000000, 0, 1)
+DBPATCH_ADD(6000001, 0, 1)
+DBPATCH_ADD(6000002, 0, 1)
+DBPATCH_ADD(6000003, 0, 1)
+DBPATCH_ADD(6000004, 0, 1)
+DBPATCH_ADD(6000005, 0, 1)
+DBPATCH_ADD(6000006, 0, 1)
+DBPATCH_ADD(6000007, 0, 1)
+DBPATCH_ADD(6000008, 0, 1)
+DBPATCH_ADD(6000009, 0, 1)
+DBPATCH_ADD(6000010, 0, 1)
+DBPATCH_ADD(6000011, 0, 1)
+DBPATCH_ADD(6000012, 0, 1)
+DBPATCH_ADD(6000013, 0, 1)
+DBPATCH_ADD(6000014, 0, 1)
+DBPATCH_ADD(6000015, 0, 1)
 
 DBPATCH_END()
diff --git a/ui/app/controllers/CControllerAuditLogList.php b/ui/app/controllers/CControllerAuditLogList.php
index f768f0a0983..6f6f19ad57b 100644
--- a/ui/app/controllers/CControllerAuditLogList.php
+++ b/ui/app/controllers/CControllerAuditLogList.php
@@ -242,6 +242,7 @@ class CControllerAuditLogList extends CController {
 			CAudit::RESOURCE_TRIGGER => _('Trigger'),
 			CAudit::RESOURCE_TRIGGER_PROTOTYPE => _('Trigger prototype'),
 			CAudit::RESOURCE_USER => _('User'),
+			CAudit::RESOURCE_USERDIRECTORY => _('User directory'),
 			CAudit::RESOURCE_USER_GROUP => _('User group'),
 			CAudit::RESOURCE_USER_ROLE => _('User role'),
 			CAudit::RESOURCE_VALUE_MAP => _('Value map')
diff --git a/ui/app/controllers/CControllerAuthenticationEdit.php b/ui/app/controllers/CControllerAuthenticationEdit.php
index e4b6d51d1c5..6e501cf0604 100644
--- a/ui/app/controllers/CControllerAuthenticationEdit.php
+++ b/ui/app/controllers/CControllerAuthenticationEdit.php
@@ -32,41 +32,36 @@ class CControllerAuthenticationEdit extends CController {
 	 */
 	protected function checkInput() {
 		$fields = [
-			'form_refresh' =>				'string',
-			'ldap_test_user' =>				'string',
-			'ldap_test_password' =>			'string',
-			'change_bind_password' =>		'in 0,1',
-			'db_authentication_type' =>		'string',
-			'authentication_type' =>		'in '.ZBX_AUTH_INTERNAL.','.ZBX_AUTH_LDAP,
-			'http_auth_enabled' =>			'in '.ZBX_AUTH_HTTP_DISABLED.','.ZBX_AUTH_HTTP_ENABLED,
-			'http_login_form' =>			'in '.ZBX_AUTH_FORM_ZABBIX.','.ZBX_AUTH_FORM_HTTP,
-			'http_strip_domains' =>			'db config.http_strip_domains',
-			'http_case_sensitive' =>		'in '.ZBX_AUTH_CASE_INSENSITIVE.','.ZBX_AUTH_CASE_SENSITIVE,
-			'ldap_case_sensitive' =>		'in '.ZBX_AUTH_CASE_INSENSITIVE.','.ZBX_AUTH_CASE_SENSITIVE,
-			'ldap_configured' =>			'in '.ZBX_AUTH_LDAP_DISABLED.','.ZBX_AUTH_LDAP_ENABLED,
-			'ldap_host' =>					'db config.ldap_host',
-			'ldap_port' =>					'int32',
-			'ldap_base_dn' =>				'db config.ldap_base_dn',
-			'ldap_bind_dn' =>				'db config.ldap_bind_dn',
-			'ldap_search_attribute' =>		'db config.ldap_search_attribute',
-			'ldap_bind_password' =>			'db config.ldap_bind_password',
-			'saml_auth_enabled' =>			'in '.ZBX_AUTH_SAML_DISABLED.','.ZBX_AUTH_SAML_ENABLED,
-			'saml_idp_entityid' =>			'db config.saml_idp_entityid',
-			'saml_sso_url' =>				'db config.saml_sso_url',
-			'saml_slo_url' =>				'db config.saml_slo_url',
-			'saml_username_attribute' =>	'db config.saml_username_attribute',
-			'saml_sp_entityid' =>			'db config.saml_sp_entityid',
-			'saml_nameid_format' =>			'db config.saml_nameid_format',
-			'saml_sign_messages' =>			'in 0,1',
-			'saml_sign_assertions' =>		'in 0,1',
-			'saml_sign_authn_requests' =>	'in 0,1',
-			'saml_sign_logout_requests' =>	'in 0,1',
-			'saml_sign_logout_responses' =>	'in 0,1',
-			'saml_encrypt_nameid' =>		'in 0,1',
-			'saml_encrypt_assertions' =>	'in 0,1',
-			'saml_case_sensitive' =>		'in '.ZBX_AUTH_CASE_INSENSITIVE.','.ZBX_AUTH_CASE_SENSITIVE,
-			'passwd_min_length' =>			'int32',
-			'passwd_check_rules' =>			'int32|ge 0|le '.(PASSWD_CHECK_CASE | PASSWD_CHECK_DIGITS | PASSWD_CHECK_SPECIAL | PASSWD_CHECK_SIMPLE)
+			'form_refresh' =>					'string',
+			'change_bind_password' =>			'in 0,1',
+			'db_authentication_type' =>			'string',
+			'authentication_type' =>			'in '.ZBX_AUTH_INTERNAL.','.ZBX_AUTH_LDAP,
+			'http_auth_enabled' =>				'in '.ZBX_AUTH_HTTP_DISABLED.','.ZBX_AUTH_HTTP_ENABLED,
+			'http_login_form' =>				'in '.ZBX_AUTH_FORM_ZABBIX.','.ZBX_AUTH_FORM_HTTP,
+			'http_strip_domains' =>				'db config.http_strip_domains',
+			'http_case_sensitive' =>			'in '.ZBX_AUTH_CASE_INSENSITIVE.','.ZBX_AUTH_CASE_SENSITIVE,
+			'ldap_configured' =>				'in '.ZBX_AUTH_LDAP_DISABLED.','.ZBX_AUTH_LDAP_ENABLED,
+			'ldap_servers' =>					'array',
+			'ldap_default_row_index' =>			'int32',
+			'ldap_case_sensitive' =>			'in '.ZBX_AUTH_CASE_INSENSITIVE.','.ZBX_AUTH_CASE_SENSITIVE,
+			'ldap_removed_userdirectoryids' =>	'array',
+			'saml_auth_enabled' =>				'in '.ZBX_AUTH_SAML_DISABLED.','.ZBX_AUTH_SAML_ENABLED,
+			'saml_idp_entityid' =>				'db config.saml_idp_entityid',
+			'saml_sso_url' =>					'db config.saml_sso_url',
+			'saml_slo_url' =>					'db config.saml_slo_url',
+			'saml_username_attribute' =>		'db config.saml_username_attribute',
+			'saml_sp_entityid' =>				'db config.saml_sp_entityid',
+			'saml_nameid_format' =>				'db config.saml_nameid_format',
+			'saml_sign_messages' =>				'in 0,1',
+			'saml_sign_assertions' =>			'in 0,1',
+			'saml_sign_authn_requests' =>		'in 0,1',
+			'saml_sign_logout_requests' =>		'in 0,1',
+			'saml_sign_logout_responses' =>		'in 0,1',
+			'saml_encrypt_nameid' =>			'in 0,1',
+			'saml_encrypt_assertions' =>		'in 0,1',
+			'saml_case_sensitive' =>			'in '.ZBX_AUTH_CASE_INSENSITIVE.','.ZBX_AUTH_CASE_SENSITIVE,
+			'passwd_min_length' =>				'int32',
+			'passwd_check_rules' =>				'int32|ge 0|le '.(PASSWD_CHECK_CASE | PASSWD_CHECK_DIGITS | PASSWD_CHECK_SPECIAL | PASSWD_CHECK_SIMPLE)
 		];
 
 		$ret = $this->validateInput($fields);
@@ -95,8 +90,6 @@ class CControllerAuthenticationEdit extends CController {
 			'action_submit' => 'authentication.update',
 			'action_passw_change' => 'authentication.edit',
 			'ldap_error' => ($ldap_status['result'] == CFrontendSetup::CHECK_OK) ? '' : $ldap_status['error'],
-			'ldap_test_password' => '',
-			'ldap_test_user' => CWebUser::$data['username'],
 			'saml_error' => ($openssl_status['result'] == CFrontendSetup::CHECK_OK) ? '' : $openssl_status['error'],
 			'change_bind_password' => 0,
 			'form_refresh' => 0
@@ -108,14 +101,9 @@ class CControllerAuthenticationEdit extends CController {
 			CAuthenticationHelper::HTTP_LOGIN_FORM,
 			CAuthenticationHelper::HTTP_STRIP_DOMAINS,
 			CAuthenticationHelper::HTTP_CASE_SENSITIVE,
-			CAuthenticationHelper::LDAP_CASE_SENSITIVE,
 			CAuthenticationHelper::LDAP_CONFIGURED,
-			CAuthenticationHelper::LDAP_HOST,
-			CAuthenticationHelper::LDAP_PORT,
-			CAuthenticationHelper::LDAP_BASE_DN,
-			CAuthenticationHelper::LDAP_BIND_DN,
-			CAuthenticationHelper::LDAP_SEARCH_ATTRIBUTE,
-			CAuthenticationHelper::LDAP_BIND_PASSWORD,
+			CAuthenticationHelper::LDAP_USERDIRECTORYID,
+			CAuthenticationHelper::LDAP_CASE_SENSITIVE,
 			CAuthenticationHelper::SAML_AUTH_ENABLED,
 			CAuthenticationHelper::SAML_IDP_ENTITYID,
 			CAuthenticationHelper::SAML_SSO_URL,
@@ -140,26 +128,16 @@ class CControllerAuthenticationEdit extends CController {
 		}
 
 		if ($this->hasInput('form_refresh')) {
-			$data['ldap_bind_password'] = '';
 			$this->getInputs($data, [
 				'form_refresh',
 				'change_bind_password',
-				'db_authentication_type',
 				'authentication_type',
 				'http_auth_enabled',
 				'http_login_form',
 				'http_strip_domains',
 				'http_case_sensitive',
-				'ldap_case_sensitive',
 				'ldap_configured',
-				'ldap_host',
-				'ldap_port',
-				'ldap_base_dn',
-				'ldap_bind_dn',
-				'ldap_search_attribute',
-				'ldap_bind_password',
-				'ldap_test_user',
-				'ldap_test_password',
+				'ldap_case_sensitive',
 				'saml_auth_enabled',
 				'saml_idp_entityid',
 				'saml_sso_url',
@@ -179,21 +157,64 @@ class CControllerAuthenticationEdit extends CController {
 				'passwd_check_rules'
 			]);
 
+			$data['ldap_servers'] = $this->getLdapServerUserGroupCount($this->getInput('ldap_servers', []));
+			$data['ldap_default_row_index'] = $this->getInput('ldap_default_row_index', 0);
+			$data['ldap_removed_userdirectoryids'] = $this->getInput('ldap_removed_userdirectoryids', []);
+
 			$data += $auth;
 		}
 		else {
 			$data += $auth;
-			$data['db_authentication_type'] = $data['authentication_type'];
-			$data['change_bind_password'] = ($data['ldap_bind_password'] === '') ? 1 : 0;
+
+			$data['ldap_servers'] = API::UserDirectory()->get([
+				'output' => ['userdirectoryid', 'name', 'description', 'host', 'port', 'base_dn', 'search_attribute',
+					'search_filter', 'start_tls', 'bind_dn'
+				],
+				'selectUsrgrps' => API_OUTPUT_COUNT,
+				'sortfield' => ['name'],
+				'sortorder' => ZBX_SORT_UP
+			]);
+
+			$data['ldap_default_row_index'] = array_search($data[CAuthenticationHelper::LDAP_USERDIRECTORYID],
+				array_column($data['ldap_servers'], 'userdirectoryid')
+			);
+			$data['ldap_removed_userdirectoryids'] = [];
 		}
 
+		unset($data[CAuthenticationHelper::LDAP_USERDIRECTORYID]);
 		$data['ldap_enabled'] = ($ldap_status['result'] == CFrontendSetup::CHECK_OK
 				&& $data['ldap_configured'] == ZBX_AUTH_LDAP_ENABLED);
 		$data['saml_enabled'] = ($openssl_status['result'] == CFrontendSetup::CHECK_OK
 				&& $data['saml_auth_enabled'] == ZBX_AUTH_SAML_ENABLED);
+		$data['db_authentication_type'] = CAuthenticationHelper::get(CAuthenticationHelper::AUTHENTICATION_TYPE);
 
 		$response = new CControllerResponseData($data);
 		$response->setTitle(_('Configuration of authentication'));
 		$this->setResponse($response);
 	}
+
+	private function getLdapServerUserGroupCount(array $ldap_servers): array {
+		$ldap_serverids = array_column($ldap_servers, 'userdirectoryid');
+
+		$db_ldap_servers = $ldap_serverids
+			? API::UserDirectory()->get([
+				'output' => ['userdirectoryid'],
+				'selectUsrgrps' => API_OUTPUT_COUNT,
+				'userdirectoryids' => $ldap_serverids,
+				'preservekeys' => true
+			])
+			: [];
+
+		foreach ($ldap_servers as &$ldap_server) {
+			$ldap_server['usrgrps'] = 0;
+
+			if (array_key_exists('userdirectoryid', $ldap_server)
+					&& array_key_exists($ldap_server['userdirectoryid'], $db_ldap_servers)) {
+				$ldap_server['usrgrps'] = $db_ldap_servers[$ldap_server['userdirectoryid']]['usrgrps'];
+			}
+		}
+		unset($ldap_server);
+
+		return $ldap_servers;
+	}
 }
diff --git a/ui/app/controllers/CControllerAuthenticationUpdate.php b/ui/app/controllers/CControllerAuthenticationUpdate.php
index 5ae1d6e3c0a..187bf3db693 100644
--- a/ui/app/controllers/CControllerAuthenticationUpdate.php
+++ b/ui/app/controllers/CControllerAuthenticationUpdate.php
@@ -35,42 +35,38 @@ class CControllerAuthenticationUpdate extends CController {
 
 	protected function checkInput() {
 		$fields = [
-			'form_refresh' =>				'string',
-			'ldap_test_user' =>				'string',
-			'ldap_test_password' =>			'string',
-			'ldap_test' =>					'in 1',
-			'db_authentication_type' =>		'int32',
-			'change_bind_password' =>		'in 0,1',
-			'authentication_type' =>		'in '.ZBX_AUTH_INTERNAL.','.ZBX_AUTH_LDAP,
-			'http_case_sensitive' =>		'in '.ZBX_AUTH_CASE_INSENSITIVE.','.ZBX_AUTH_CASE_SENSITIVE,
-			'ldap_case_sensitive' =>		'in '.ZBX_AUTH_CASE_INSENSITIVE.','.ZBX_AUTH_CASE_SENSITIVE,
-			'ldap_configured' =>			'in '.ZBX_AUTH_LDAP_DISABLED.','.ZBX_AUTH_LDAP_ENABLED,
-			'ldap_host' =>					'db config.ldap_host',
-			'ldap_port' =>					'int32',
-			'ldap_base_dn' =>				'db config.ldap_base_dn',
-			'ldap_bind_dn' =>				'db config.ldap_bind_dn',
-			'ldap_search_attribute' =>		'db config.ldap_search_attribute',
-			'ldap_bind_password' =>			'db config.ldap_bind_password',
-			'http_auth_enabled' =>			'in '.ZBX_AUTH_HTTP_DISABLED.','.ZBX_AUTH_HTTP_ENABLED,
-			'http_login_form' =>			'in '.ZBX_AUTH_FORM_ZABBIX.','.ZBX_AUTH_FORM_HTTP,
-			'http_strip_domains' =>			'db config.http_strip_domains',
-			'saml_auth_enabled' =>			'in '.ZBX_AUTH_SAML_DISABLED.','.ZBX_AUTH_SAML_ENABLED,
-			'saml_idp_entityid' =>			'db config.saml_idp_entityid',
-			'saml_sso_url' =>				'db config.saml_sso_url',
-			'saml_slo_url' =>				'db config.saml_slo_url',
-			'saml_username_attribute' =>	'db config.saml_username_attribute',
-			'saml_sp_entityid' =>			'db config.saml_sp_entityid',
-			'saml_nameid_format' =>			'db config.saml_nameid_format',
-			'saml_sign_messages' =>			'in 0,1',
-			'saml_sign_assertions' =>		'in 0,1',
-			'saml_sign_authn_requests' =>	'in 0,1',
-			'saml_sign_logout_requests' =>	'in 0,1',
-			'saml_sign_logout_responses' =>	'in 0,1',
-			'saml_encrypt_nameid' =>		'in 0,1',
-			'saml_encrypt_assertions' =>	'in 0,1',
-			'saml_case_sensitive' =>		'in '.ZBX_AUTH_CASE_INSENSITIVE.','.ZBX_AUTH_CASE_SENSITIVE,
-			'passwd_min_length' =>			'int32',
-			'passwd_check_rules' =>			'array'
+			'form_refresh' =>					'string',
+			'ldap_test_user' =>					'string',
+			'ldap_test_password' =>				'string',
+			'ldap_test' =>						'in 1',
+			'change_bind_password' =>			'in 0,1',
+			'authentication_type' =>			'in '.ZBX_AUTH_INTERNAL.','.ZBX_AUTH_LDAP,
+			'http_case_sensitive' =>			'in '.ZBX_AUTH_CASE_INSENSITIVE.','.ZBX_AUTH_CASE_SENSITIVE,
+			'ldap_configured' =>				'in '.ZBX_AUTH_LDAP_DISABLED.','.ZBX_AUTH_LDAP_ENABLED,
+			'ldap_servers' =>					'array',
+			'ldap_default_row_index' =>			'int32',
+			'ldap_case_sensitive' =>			'in '.ZBX_AUTH_CASE_INSENSITIVE.','.ZBX_AUTH_CASE_SENSITIVE,
+			'ldap_removed_userdirectoryids' =>	'array',
+			'http_auth_enabled' =>				'in '.ZBX_AUTH_HTTP_DISABLED.','.ZBX_AUTH_HTTP_ENABLED,
+			'http_login_form' =>				'in '.ZBX_AUTH_FORM_ZABBIX.','.ZBX_AUTH_FORM_HTTP,
+			'http_strip_domains' =>				'db config.http_strip_domains',
+			'saml_auth_enabled' =>				'in '.ZBX_AUTH_SAML_DISABLED.','.ZBX_AUTH_SAML_ENABLED,
+			'saml_idp_entityid' =>				'db config.saml_idp_entityid',
+			'saml_sso_url' =>					'db config.saml_sso_url',
+			'saml_slo_url' =>					'db config.saml_slo_url',
+			'saml_username_attribute' =>		'db config.saml_username_attribute',
+			'saml_sp_entityid' =>				'db config.saml_sp_entityid',
+			'saml_nameid_format' =>				'db config.saml_nameid_format',
+			'saml_sign_messages' =>				'in 0,1',
+			'saml_sign_assertions' =>			'in 0,1',
+			'saml_sign_authn_requests' =>		'in 0,1',
+			'saml_sign_logout_requests' =>		'in 0,1',
+			'saml_sign_logout_responses' =>		'in 0,1',
+			'saml_encrypt_nameid' =>			'in 0,1',
+			'saml_encrypt_assertions' =>		'in 0,1',
+			'saml_case_sensitive' =>			'in '.ZBX_AUTH_CASE_INSENSITIVE.','.ZBX_AUTH_CASE_SENSITIVE,
+			'passwd_min_length' =>				'int32',
+			'passwd_check_rules' =>				'array'
 		];
 
 		$ret = $this->validateInput($fields);
@@ -113,78 +109,33 @@ class CControllerAuthenticationUpdate extends CController {
 	 *
 	 * @return bool
 	 */
-	private function validateLdap() {
-		$is_valid = true;
-		$ldap_status = (new CFrontendSetup())->checkPhpLdapModule();
-		$ldap_fields = ['ldap_host', 'ldap_port', 'ldap_base_dn', 'ldap_search_attribute', 'ldap_configured'];
-		$ldap_auth_original = [
-			'ldap_host' => CAuthenticationHelper::get(CAuthenticationHelper::LDAP_HOST),
-			'ldap_port' => CAuthenticationHelper::get(CAuthenticationHelper::LDAP_PORT),
-			'ldap_base_dn' => CAuthenticationHelper::get(CAuthenticationHelper::LDAP_BASE_DN),
-			'ldap_search_attribute' => CAuthenticationHelper::get(CAuthenticationHelper::LDAP_SEARCH_ATTRIBUTE),
-			'ldap_configured' => CAuthenticationHelper::get(CAuthenticationHelper::LDAP_CONFIGURED),
-			'ldap_bind_dn' => CAuthenticationHelper::get(CAuthenticationHelper::LDAP_BIND_DN),
-			'ldap_bind_password' => CAuthenticationHelper::get(CAuthenticationHelper::LDAP_BIND_PASSWORD)
-		];
-		$ldap_auth = $ldap_auth_original;
-		$this->getInputs($ldap_auth, array_merge($ldap_fields, ['ldap_bind_dn', 'ldap_bind_password']));
-		$ldap_auth_changed = array_diff_assoc($ldap_auth, $ldap_auth_original);
+	private function validateLdap(): bool {
+		$ldap_enabled = $this->getInput('ldap_configured', ZBX_AUTH_LDAP_DISABLED) == ZBX_AUTH_LDAP_ENABLED;
+		$ldap_servers = $this->getInput('ldap_servers', []);
 
-		if (!$ldap_auth_changed && !$this->hasInput('ldap_test')) {
-			return $is_valid;
-		}
+		if ($ldap_enabled) {
+			$ldap_status = (new CFrontendSetup())->checkPhpLdapModule();
 
-		if ($this->getInput('ldap_bind_password', '') !== '') {
-			$ldap_fields[] = 'ldap_bind_dn';
-		}
-
-		foreach ($ldap_fields as $field) {
-			if (trim($ldap_auth[$field]) === '') {
-				CMessageHelper::setErrorTitle(_s('Incorrect value for field "%1$s": %2$s.', $field, _('cannot be empty')));
-				$is_valid = false;
-				break;
+			if ($ldap_status['result'] != CFrontendSetup::CHECK_OK) {
+				CMessageHelper::setErrorTitle($ldap_status['error']);
+				return false;
 			}
-		}
-
-		if ($is_valid
-				&& ($ldap_auth['ldap_port'] < ZBX_MIN_PORT_NUMBER
-					|| $ldap_auth['ldap_port'] > ZBX_MAX_PORT_NUMBER)) {
-			CMessageHelper::setErrorTitle(_s(
-				'Incorrect value "%1$s" for "%2$s" field: must be between %3$s and %4$s.', $this->getInput('ldap_port'),
-				'ldap_port', ZBX_MIN_PORT_NUMBER, ZBX_MAX_PORT_NUMBER
-			));
-			$is_valid = false;
-		}
 
-		if ($ldap_status['result'] != CFrontendSetup::CHECK_OK) {
-			CMessageHelper::setErrorTitle($ldap_status['error']);
-			$is_valid = false;
+			if (!$ldap_servers) {
+				CMessageHelper::setErrorTitle(_('At least one LDAP server must exist.'));
+				return false;
+			}
 		}
-		elseif ($is_valid) {
-			$ldap_validator = new CLdapAuthValidator([
-				'conf' => [
-					'host' => $ldap_auth['ldap_host'],
-					'port' => $ldap_auth['ldap_port'],
-					'base_dn' => $ldap_auth['ldap_base_dn'],
-					'bind_dn' => $ldap_auth['ldap_bind_dn'],
-					'bind_password' => $ldap_auth['ldap_bind_password'],
-					'search_attribute' => $ldap_auth['ldap_search_attribute']
-				],
-				'detailed_errors' => true
-			]);
-
-			$login = $ldap_validator->validate([
-				'username' => $this->getInput('ldap_test_user', CWebUser::$data['username']),
-				'password' => $this->getInput('ldap_test_password', '')
-			]);
 
-			if (!$login) {
-				CMessageHelper::setErrorTitle($ldap_validator->getError());
-				$is_valid = false;
-			}
+		if ($ldap_servers
+				&& (!$this->hasInput('ldap_default_row_index')
+					|| !array_key_exists($this->getInput('ldap_default_row_index'), $ldap_servers)
+				)) {
+			CMessageHelper::setErrorTitle(_('Default LDAP server must be specified.'));
+			return false;
 		}
 
-		return $is_valid;
+		return true;
 	}
 
 	/**
@@ -251,9 +202,11 @@ class CControllerAuthenticationUpdate extends CController {
 	}
 
 	protected function doAction() {
-		$auth_valid = ($this->getInput('ldap_configured', '') == ZBX_AUTH_LDAP_ENABLED)
-			? $this->validateLdap()
-			: $this->validateDefaultAuth();
+		$auth_valid = $this->validateDefaultAuth();
+
+		if ($auth_valid && !$this->validateLdap()) {
+			$auth_valid = false;
+		}
 
 		if ($auth_valid && $this->getInput('saml_auth_enabled', ZBX_AUTH_SAML_DISABLED) == ZBX_AUTH_SAML_ENABLED) {
 			if (!$this->validateSamlAuth()) {
@@ -261,16 +214,14 @@ class CControllerAuthenticationUpdate extends CController {
 			}
 		}
 
-		if (!$auth_valid) {
-			$this->response->setFormData($this->getInputAll());
-			$this->setResponse($this->response);
-
-			return;
+		if ($auth_valid) {
+			[$auth_valid, $ldap_userdirectoryid] = $this->processLdapServers(
+				$this->getInput('ldap_servers', []),
+				$this->getInput('ldap_default_row_index', 0)
+			);
 		}
 
-		// Only ZBX_AUTH_LDAP have 'Test' option.
-		if ($this->hasInput('ldap_test')) {
-			CMessageHelper::setSuccessTitle(_('LDAP login successful'));
+		if (!$auth_valid) {
 			$this->response->setFormData($this->getInputAll());
 			$this->setResponse($this->response);
 
@@ -283,14 +234,9 @@ class CControllerAuthenticationUpdate extends CController {
 			CAuthenticationHelper::HTTP_LOGIN_FORM,
 			CAuthenticationHelper::HTTP_STRIP_DOMAINS,
 			CAuthenticationHelper::HTTP_CASE_SENSITIVE,
-			CAuthenticationHelper::LDAP_CASE_SENSITIVE,
 			CAuthenticationHelper::LDAP_CONFIGURED,
-			CAuthenticationHelper::LDAP_HOST,
-			CAuthenticationHelper::LDAP_PORT,
-			CAuthenticationHelper::LDAP_BASE_DN,
-			CAuthenticationHelper::LDAP_BIND_DN,
-			CAuthenticationHelper::LDAP_SEARCH_ATTRIBUTE,
-			CAuthenticationHelper::LDAP_BIND_PASSWORD,
+			CAuthenticationHelper::LDAP_USERDIRECTORYID,
+			CAuthenticationHelper::LDAP_CASE_SENSITIVE,
 			CAuthenticationHelper::SAML_AUTH_ENABLED,
 			CAuthenticationHelper::SAML_IDP_ENTITYID,
 			CAuthenticationHelper::SAML_SSO_URL,
@@ -317,6 +263,8 @@ class CControllerAuthenticationUpdate extends CController {
 		$fields = [
 			'authentication_type' => ZBX_AUTH_INTERNAL,
 			'ldap_configured' => ZBX_AUTH_LDAP_DISABLED,
+			'ldap_userdirectoryid' => $ldap_userdirectoryid,
+			'ldap_case_sensitive' => ZBX_AUTH_CASE_INSENSITIVE,
 			'http_auth_enabled' => ZBX_AUTH_HTTP_DISABLED,
 			'saml_auth_enabled' => ZBX_AUTH_SAML_DISABLED,
 			'passwd_min_length' => DB::getDefault('config', 'passwd_min_length'),
@@ -331,24 +279,6 @@ class CControllerAuthenticationUpdate extends CController {
 			];
 		}
 
-		if ($this->getInput('ldap_configured', ZBX_AUTH_LDAP_DISABLED) == ZBX_AUTH_LDAP_ENABLED) {
-			$fields += [
-				'ldap_host' => '',
-				'ldap_port' => '',
-				'ldap_base_dn' => '',
-				'ldap_bind_dn' => '',
-				'ldap_search_attribute' => '',
-				'ldap_case_sensitive' => 0
-			];
-
-			if ($this->hasInput('ldap_bind_password')) {
-				$fields['ldap_bind_password'] = '';
-			}
-			else {
-				unset($auth[CAuthenticationHelper::LDAP_BIND_PASSWORD]);
-			}
-		}
-
 		if ($this->getInput('saml_auth_enabled', ZBX_AUTH_SAML_DISABLED) == ZBX_AUTH_SAML_ENABLED) {
 			$fields += [
 				'saml_idp_entityid' => '',
@@ -379,33 +309,92 @@ class CControllerAuthenticationUpdate extends CController {
 		}
 
 		$data = array_diff_assoc($data, $auth);
-
-		if (array_key_exists('ldap_bind_dn', $data) && trim($data['ldap_bind_dn']) === '') {
-			$data['ldap_bind_password'] = '';
-		}
+		$result = true;
 
 		if ($data) {
 			$result = API::Authentication()->update($data);
 
-			if ($result) {
-				if (array_key_exists('authentication_type', $data)) {
-					$this->invalidateSessions();
-				}
-
-				CMessageHelper::setSuccessTitle(_('Authentication settings updated'));
+			if ($result && array_key_exists('authentication_type', $data)) {
+				$this->invalidateSessions();
 			}
-			else {
-				$this->response->setFormData($this->getInputAll());
-				CMessageHelper::setErrorTitle(_('Cannot update authentication'));
+		}
+
+		if ($result) {
+			$del_userdirectoryids = $this->getInput('ldap_removed_userdirectoryids', []);
+
+			if ($del_userdirectoryids) {
+				$result = API::UserDirectory()->delete($del_userdirectoryids);
 			}
 		}
-		else {
+
+		if ($result) {
 			CMessageHelper::setSuccessTitle(_('Authentication settings updated'));
 		}
+		else {
+			$this->response->setFormData($this->getInputAll());
+			CMessageHelper::setErrorTitle(_('Cannot update authentication'));
+		}
 
 		$this->setResponse($this->response);
 	}
 
+	/**
+	 * Updates existing LDAP servers, creates new ones, removes deleted ones.
+	 *
+	 * @param array $ldap_servers
+	 * @param int $default_row_index
+	 *
+	 * @return array
+	 */
+	private function processLdapServers(array $ldap_servers, int $default_row_index): array {
+		$ins_ldap_servers = [];
+		$upd_ldap_servers = [];
+
+		$ldap_userdirectoryid = 0;
+		$default_ldap_mapping_index = null;
+
+		foreach ($ldap_servers as $row_index => $ldap_server) {
+			if (array_key_exists('userdirectoryid', $ldap_server)) {
+				$upd_ldap_servers[] = $ldap_server;
+
+				if ($default_row_index == $row_index) {
+					$ldap_userdirectoryid = $ldap_server['userdirectoryid'];
+				}
+			}
+			else {
+				$ins_ldap_servers[] = $ldap_server;
+
+				if ($default_row_index == $row_index) {
+					$default_ldap_mapping_index = count($ins_ldap_servers) - 1;
+				}
+			}
+		}
+
+		if ($upd_ldap_servers && !API::UserDirectory()->update($upd_ldap_servers)) {
+			return [false, $ldap_userdirectoryid];
+		}
+
+		if ($ins_ldap_servers) {
+			$inserted_ids = API::UserDirectory()->create($ins_ldap_servers);
+
+			if (!$inserted_ids) {
+				return [false, $ldap_userdirectoryid];
+			}
+
+			if ($default_ldap_mapping_index !== null) {
+				$ldap_userdirectoryid = $inserted_ids['userdirectoryids'][$default_ldap_mapping_index];
+			}
+		}
+
+		if ($ldap_servers && $ldap_userdirectoryid === 0) {
+			CMessageHelper::setErrorTitle(_('Failed to select default LDAP server.'));
+
+			return [false, $ldap_userdirectoryid];
+		}
+
+		return [true, $ldap_userdirectoryid];
+	}
+
 	/**
 	 * Mark all active GROUP_GUI_ACCESS_INTERNAL sessions, except current user sessions, as ZBX_SESSION_PASSIVE.
 	 *
diff --git a/ui/app/controllers/CControllerPopupLdapCheck.php b/ui/app/controllers/CControllerPopupLdapCheck.php
new file mode 100644
index 00000000000..4f7a4bb7eb1
--- /dev/null
+++ b/ui/app/controllers/CControllerPopupLdapCheck.php
@@ -0,0 +1,97 @@
+<?php declare(strict_types = 1);
+/*
+** Zabbix
+** Copyright (C) 2001-2022 Zabbix SIA
+**
+** This program is free software; you can redistribute it and/or modify
+** it under the terms of the GNU General Public License as published by
+** the Free Software Foundation; either version 2 of the License, or
+** (at your option) any later version.
+**
+** This program is distributed in the hope that it will be useful,
+** but WITHOUT ANY WARRANTY; without even the implied warranty of
+** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+** GNU General Public License for more details.
+**
+** You should have received a copy of the GNU General Public License
+** along with this program; if not, write to the Free Software
+** Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+**/
+
+
+class CControllerPopupLdapCheck extends CController {
+
+	protected function init(): void {
+		$this->disableSIDValidation();
+
+		$this->setPostContentType(self::POST_CONTENT_TYPE_JSON);
+	}
+
+	protected function checkInput(): bool {
+		$fields = [
+			'row_index' =>			'required|int32',
+			'userdirectoryid' =>	'db userdirectory.userdirectoryid',
+			'name' =>				'required|db userdirectory.name|not_empty',
+			'host' =>				'required|db userdirectory.host|not_empty',
+			'port' =>				'required|db userdirectory.port|ge '.ZBX_MIN_PORT_NUMBER.'|le '.ZBX_MAX_PORT_NUMBER,
+			'base_dn' =>			'required|db userdirectory.base_dn|not_empty',
+			'bind_dn' =>			'db userdirectory.bind_dn',
+			'bind_password' =>		'db userdirectory.bind_password',
+			'search_attribute' =>	'required|db userdirectory.search_attribute|not_empty',
+			'start_tls' =>			'in '.ZBX_AUTH_START_TLS_OFF.','.ZBX_AUTH_START_TLS_ON,
+			'search_filter' =>		'db userdirectory.search_filter',
+			'description' =>		'db userdirectory.description'
+		];
+
+		$ret = $this->validateInput($fields);
+
+		if (!$ret) {
+			$this->setResponse(
+				new CControllerResponseData(['main_block' => json_encode([
+					'error' => [
+						'title' => _('Invalid LDAP configuration'),
+						'messages' => array_column(get_and_clear_messages(), 'message')
+					]
+				])])
+			);
+		}
+
+		return $ret;
+	}
+
+	protected function checkPermissions(): bool {
+		return $this->checkAccess(CRoleHelper::UI_ADMINISTRATION_AUTHENTICATION);
+	}
+
+	protected function doAction(): void {
+		$data = [
+			'body' => [
+				'row_index' => $this->getInput('row_index'),
+				'name' => $this->getInput('name'),
+				'host' => $this->getInput('host'),
+				'port' => $this->getInput('port'),
+				'base_dn' => $this->getInput('base_dn'),
+				'search_attribute' => $this->getInput('search_attribute'),
+				'start_tls' => $this->getInput('start_tls', ZBX_AUTH_START_TLS_OFF),
+				'bind_dn' => $this->getInput('bind_dn', ''),
+				'description' => $this->getInput('description', ''),
+				'search_filter' => $this->getInput('search_filter', '')
+			]
+		];
+
+		if ($this->hasInput('userdirectoryid')) {
+			$data['body']['userdirectoryid'] = $this->getInput('userdirectoryid');
+		}
+
+		if ($this->hasInput('bind_password')) {
+			$data['body']['bind_password'] = $this->getInput('bind_password');
+		}
+
+		if ($this->getDebugMode() == GROUP_DEBUG_MODE_ENABLED) {
+			CProfiler::getInstance()->stop();
+			$data['debug'] = CProfiler::getInstance()->make()->toString();
+		}
+
+		$this->setResponse(new CControllerResponseData(['main_block' => json_encode($data)]));
+	}
+}
diff --git a/ui/app/controllers/CControllerPopupLdapEdit.php b/ui/app/controllers/CControllerPopupLdapEdit.php
new file mode 100644
index 00000000000..f565e131655
--- /dev/null
+++ b/ui/app/controllers/CControllerPopupLdapEdit.php
@@ -0,0 +1,95 @@
+<?php declare(strict_types = 1);
+/*
+** Zabbix
+** Copyright (C) 2001-2022 Zabbix SIA
+**
+** This program is free software; you can redistribute it and/or modify
+** it under the terms of the GNU General Public License as published by
+** the Free Software Foundation; either version 2 of the License, or
+** (at your option) any later version.
+**
+** This program is distributed in the hope that it will be useful,
+** but WITHOUT ANY WARRANTY; without even the implied warranty of
+** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+** GNU General Public License for more details.
+**
+** You should have received a copy of the GNU General Public License
+** along with this program; if not, write to the Free Software
+** Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+**/
+
+
+class CControllerPopupLdapEdit extends CController {
+
+	protected function checkInput(): bool {
+		$fields = [
+			'row_index' =>			'required|int32',
+			'userdirectoryid' =>	'db userdirectory.userdirectoryid',
+			'name' =>				'db userdirectory.name',
+			'host' =>				'db userdirectory.host',
+			'port' =>				'db userdirectory.port|ge '.ZBX_MIN_PORT_NUMBER.'|le '.ZBX_MAX_PORT_NUMBER,
+			'base_dn' =>			'db userdirectory.base_dn',
+			'bind_dn' =>			'db userdirectory.bind_dn',
+			'bind_password' =>		'db userdirectory.bind_password',
+			'search_attribute' =>	'db userdirectory.search_attribute',
+			'start_tls' =>			'in '.ZBX_AUTH_START_TLS_OFF.','.ZBX_AUTH_START_TLS_ON,
+			'search_filter' =>		'db userdirectory.search_filter',
+			'case_sensitive' =>		'in '.ZBX_AUTH_CASE_INSENSITIVE.','.ZBX_AUTH_CASE_SENSITIVE,
+			'description' =>		'db userdirectory.description',
+			'add_ldap_server' =>	'in 0,1'
+		];
+
+		$ret = $this->validateInput($fields);
+
+		if (!$ret) {
+			$this->setResponse(
+				(new CControllerResponseData([
+					'main_block' => json_encode(['errors' => getMessages()->toString()])
+				]))->disableView()
+			);
+		}
+
+		return $ret;
+	}
+
+	/**
+	 * @throws APIException
+	 */
+	protected function checkPermissions(): bool {
+		return $this->checkAccess(CRoleHelper::UI_ADMINISTRATION_AUTHENTICATION);
+	}
+
+	/**
+	 * @throws Exception
+	 */
+	protected function doAction(): void {
+		$data = [
+			'row_index' => $this->getInput('row_index', 0),
+			'name' => $this->getInput('name', ''),
+			'host' => $this->getInput('host', ''),
+			'port' => $this->getInput('port', '389'),
+			'base_dn' => $this->getInput('base_dn', ''),
+			'search_attribute' => $this->getInput('search_attribute', ''),
+			'start_tls' => $this->getInput('start_tls', ZBX_AUTH_START_TLS_OFF),
+			'bind_dn' => $this->getInput('bind_dn', ''),
+			'description' => $this->getInput('description', ''),
+			'search_filter' => $this->getInput('search_filter', ''),
+			'add_ldap_server' => $this->getInput('add_ldap_server', 1),
+			'user' => [
+				'debug_mode' => $this->getDebugMode()
+			]
+		];
+
+		if ($this->hasInput('userdirectoryid')) {
+			$data['userdirectoryid'] = $this->getInput('userdirectoryid');
+		}
+
+		if ($this->hasInput('bind_password')) {
+			$data['bind_password'] = $this->getInput('bind_password');
+		}
+
+		$data['advanced_configuration'] = $data['start_tls'] != ZBX_AUTH_START_TLS_OFF || $data['search_filter'] !== '';
+
+		$this->setResponse(new CControllerResponseData($data));
+	}
+}
diff --git a/ui/app/controllers/CControllerPopupLdapTestEdit.php b/ui/app/controllers/CControllerPopupLdapTestEdit.php
new file mode 100644
index 00000000000..e067fcf7bad
--- /dev/null
+++ b/ui/app/controllers/CControllerPopupLdapTestEdit.php
@@ -0,0 +1,69 @@
+<?php declare(strict_types = 1);
+/*
+** Zabbix
+** Copyright (C) 2001-2022 Zabbix SIA
+**
+** This program is free software; you can redistribute it and/or modify
+** it under the terms of the GNU General Public License as published by
+** the Free Software Foundation; either version 2 of the License, or
+** (at your option) any later version.
+**
+** This program is distributed in the hope that it will be useful,
+** but WITHOUT ANY WARRANTY; without even the implied warranty of
+** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+** GNU General Public License for more details.
+**
+** You should have received a copy of the GNU General Public License
+** along with this program; if not, write to the Free Software
+** Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+**/
+
+
+class CControllerPopupLdapTestEdit extends CController {
+
+	protected function checkInput(): bool {
+		$fields = [
+			'userdirectoryid' =>	'db userdirectory.userdirectoryid',
+			'host' =>				'required|db userdirectory.host|not_empty',
+			'port' =>				'required|db userdirectory.port|ge '.ZBX_MIN_PORT_NUMBER.'|le '.ZBX_MAX_PORT_NUMBER,
+			'base_dn' =>			'required|db userdirectory.base_dn|not_empty',
+			'bind_dn' =>			'db userdirectory.bind_dn',
+			'bind_password' =>		'db userdirectory.bind_password',
+			'search_attribute' =>	'required|db userdirectory.search_attribute|not_empty',
+			'start_tls' =>			'in '.ZBX_AUTH_START_TLS_OFF.','.ZBX_AUTH_START_TLS_ON,
+			'search_filter' =>		'db userdirectory.search_filter'
+		];
+
+		$ret = $this->validateInput($fields);
+
+		if (!$ret) {
+			$this->setResponse(
+				(new CControllerResponseData([
+					'main_block' => json_encode(['errors' => getMessages()->toString()])
+				]))->disableView()
+			);
+		}
+
+		return $ret;
+	}
+
+	protected function checkPermissions(): bool {
+		return $this->checkAccess(CRoleHelper::UI_ADMINISTRATION_AUTHENTICATION);
+	}
+
+	protected function doAction(): void {
+		$data = [
+			'ldap_config' => [],
+			'user' => [
+				'debug_mode' => $this->getDebugMode()
+			],
+			'test_username' => CWebUser::$data['username']
+		];
+
+		$this->getInputs($data['ldap_config'], ['userdirectoryid', 'host', 'port', 'base_dn', 'bind_dn',
+			'bind_password', 'search_attribute', 'start_tls', 'search_filter','test_username', 'test_password'
+		]);
+
+		$this->setResponse(new CControllerResponseData($data));
+	}
+}
diff --git a/ui/app/controllers/CControllerPopupLdapTestSend.php b/ui/app/controllers/CControllerPopupLdapTestSend.php
new file mode 100644
index 00000000000..b982e5e6302
--- /dev/null
+++ b/ui/app/controllers/CControllerPopupLdapTestSend.php
@@ -0,0 +1,91 @@
+<?php declare(strict_types = 1);
+/*
+** Zabbix
+** Copyright (C) 2001-2022 Zabbix SIA
+**
+** This program is free software; you can redistribute it and/or modify
+** it under the terms of the GNU General Public License as published by
+** the Free Software Foundation; either version 2 of the License, or
+** (at your option) any later version.
+**
+** This program is distributed in the hope that it will be useful,
+** but WITHOUT ANY WARRANTY; without even the implied warranty of
+** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+** GNU General Public License for more details.
+**
+** You should have received a copy of the GNU General Public License
+** along with this program; if not, write to the Free Software
+** Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+**/
+
+
+class CControllerPopupLdapTestSend extends CController {
+
+	protected function init(): void {
+		$this->setPostContentType(self::POST_CONTENT_TYPE_JSON);
+	}
+
+	protected function checkInput(): bool {
+		$fields = [
+			'userdirectoryid' =>	'db userdirectory.userdirectoryid',
+			'host' =>				'required|db userdirectory.host|not_empty',
+			'port' =>				'required|db userdirectory.port|ge '.ZBX_MIN_PORT_NUMBER.'|le '.ZBX_MAX_PORT_NUMBER,
+			'base_dn' =>			'required|db userdirectory.base_dn|not_empty',
+			'bind_dn' =>			'db userdirectory.bind_dn',
+			'bind_password' =>		'db userdirectory.bind_password',
+			'search_attribute' =>	'required|db userdirectory.search_attribute|not_empty',
+			'start_tls' =>			'in '.ZBX_AUTH_START_TLS_OFF.','.ZBX_AUTH_START_TLS_ON,
+			'search_filter' =>		'db userdirectory.search_filter',
+			'test_username' =>		'required|string|not_empty',
+			'test_password' =>		'required|string|not_empty'
+		];
+
+		$ret = $this->validateInput($fields);
+
+		if (!$ret) {
+			$this->setResponse(
+				new CControllerResponseData(['main_block' => json_encode([
+					'error' => [
+						'title' => _('Invalid LDAP configuration'),
+						'messages' => array_column(get_and_clear_messages(), 'message')
+					]
+				])])
+			);
+		}
+
+		return $ret;
+	}
+
+	protected function checkPermissions(): bool {
+		return $this->checkAccess(CRoleHelper::UI_ADMINISTRATION_AUTHENTICATION);
+	}
+
+	protected function doAction(): void {
+		$ldap_test_object = [];
+		$this->getInputs($ldap_test_object, ['userdirectoryid', 'host', 'port', 'base_dn', 'bind_dn', 'bind_password',
+			'search_attribute', 'start_tls', 'search_filter','test_username', 'test_password'
+		]);
+
+		$result = API::UserDirectory()->test($ldap_test_object);
+
+		$output = [];
+
+		if ($result) {
+			$success = ['title' => _('Login successful')];
+
+			if ($messages = get_and_clear_messages()) {
+				$success['messages'] = array_column($messages, 'message');
+			}
+
+			$output['success'] = $success;
+		}
+		else {
+			$output['error'] = [
+				'title' => _('Login failed'),
+				'messages' => array_column(get_and_clear_messages(), 'message')
+			];
+		}
+
+		$this->setResponse(new CControllerResponseData(['main_block' => json_encode($output)]));
+	}
+}
diff --git a/ui/app/controllers/CControllerUsergroupCreate.php b/ui/app/controllers/CControllerUsergroupCreate.php
index cd83ddfb916..6c3c7d975a9 100644
--- a/ui/app/controllers/CControllerUsergroupCreate.php
+++ b/ui/app/controllers/CControllerUsergroupCreate.php
@@ -23,19 +23,20 @@ class CControllerUsergroupCreate extends CController {
 
 	protected function checkInput() {
 		$fields = [
-			'name'            => 'required|not_empty|db usrgrp.name',
-			'userids'         => 'array_db users.userid',
-			'gui_access'      => 'db usrgrp.gui_access|in '.implode(',', [GROUP_GUI_ACCESS_SYSTEM, GROUP_GUI_ACCESS_INTERNAL, GROUP_GUI_ACCESS_LDAP, GROUP_GUI_ACCESS_DISABLED]),
-			'users_status'    => 'db usrgrp.users_status|in '.GROUP_STATUS_ENABLED.','.GROUP_STATUS_DISABLED,
-			'debug_mode'      => 'db usrgrp.debug_mode|in '.GROUP_DEBUG_MODE_ENABLED.','.GROUP_DEBUG_MODE_DISABLED,
+			'name' =>				'required|not_empty|db usrgrp.name',
+			'userids' =>			'array_db users.userid',
+			'gui_access' =>			'db usrgrp.gui_access|in '.implode(',', [GROUP_GUI_ACCESS_SYSTEM, GROUP_GUI_ACCESS_INTERNAL, GROUP_GUI_ACCESS_LDAP, GROUP_GUI_ACCESS_DISABLED]),
+			'users_status' =>		'db usrgrp.users_status|in '.GROUP_STATUS_ENABLED.','.GROUP_STATUS_DISABLED,
+			'debug_mode' =>			'db usrgrp.debug_mode|in '.GROUP_DEBUG_MODE_ENABLED.','.GROUP_DEBUG_MODE_DISABLED,
+			'userdirectoryid' =>	'db usrgrp.userdirectoryid',
 
-			'group_rights'    => 'array',
-			'tag_filters'     => 'array',
+			'group_rights' =>		'array',
+			'tag_filters' =>		'array',
 
-			'new_group_right' => 'array',
-			'new_tag_filter'  => 'array',
+			'new_group_right' =>	'array',
+			'new_tag_filter' =>		'array',
 
-			'form_refresh'    => 'int32'
+			'form_refresh' =>		'int32'
 		];
 
 		$ret = $this->validateInput($fields);
@@ -71,7 +72,9 @@ class CControllerUsergroupCreate extends CController {
 			'rights' => []
 		];
 
-		$this->getInputs($user_group, ['name', 'users_status', 'gui_access', 'debug_mode', 'tag_filters']);
+		$this->getInputs($user_group, ['name', 'users_status', 'gui_access', 'debug_mode', 'tag_filters',
+			'userdirectoryid'
+		]);
 
 		$group_rights = applyHostGroupRights($this->getInput('group_rights', []));
 
diff --git a/ui/app/controllers/CControllerUsergroupEdit.php b/ui/app/controllers/CControllerUsergroupEdit.php
index 4e1ee157f8f..ea04abf456f 100644
--- a/ui/app/controllers/CControllerUsergroupEdit.php
+++ b/ui/app/controllers/CControllerUsergroupEdit.php
@@ -64,7 +64,7 @@ class CControllerUsergroupEdit extends CController {
 
 		if ($this->hasInput('usrgrpid')) {
 			$user_groups = API::UserGroup()->get([
-				'output' => ['name', 'gui_access', 'users_status', 'debug_mode'],
+				'output' => ['name', 'gui_access', 'users_status', 'debug_mode', 'userdirectoryid'],
 				'selectTagFilters' => ['groupid', 'tag', 'value'],
 				'usrgrpids' => $this->getInput('usrgrpid'),
 				'editable' => true
@@ -87,6 +87,7 @@ class CControllerUsergroupEdit extends CController {
 			'usrgrpid' => 0,
 			'name' => $db_defaults['name'],
 			'gui_access' => $db_defaults['gui_access'],
+			'userdirectoryid' => 0,
 			'users_status' => $db_defaults['users_status'],
 			'debug_mode' => $db_defaults['debug_mode'],
 			'form_refresh' => 0
@@ -99,6 +100,7 @@ class CControllerUsergroupEdit extends CController {
 			$data['gui_access'] = $this->user_group['gui_access'];
 			$data['users_status'] = $this->user_group['users_status'];
 			$data['debug_mode'] = $this->user_group['debug_mode'];
+			$data['userdirectoryid'] = $this->user_group['userdirectoryid'];
 		}
 
 		// overwrite with input variables
@@ -126,6 +128,14 @@ class CControllerUsergroupEdit extends CController {
 
 		$data['can_update_group'] = (!$this->hasInput('usrgrpid') || granted2update_group($this->getInput('usrgrpid')));
 
+		if ($data['can_update_group']) {
+			$userdirectories = API::UserDirectory()->get([
+				'output' => ['userdirectoryid', 'name']
+			]);
+			CArrayHelper::sort($userdirectories, ['name']);
+			$data['userdirectories'] = array_column($userdirectories, 'name', 'userdirectoryid');
+		}
+
 		$response = new CControllerResponseData($data);
 		$response->setTitle(_('Configuration of user groups'));
 		$this->setResponse($response);
diff --git a/ui/app/controllers/CControllerUsergroupUpdate.php b/ui/app/controllers/CControllerUsergroupUpdate.php
index 755afc8c7ef..7c399cc6922 100644
--- a/ui/app/controllers/CControllerUsergroupUpdate.php
+++ b/ui/app/controllers/CControllerUsergroupUpdate.php
@@ -23,20 +23,21 @@ class CControllerUsergroupUpdate extends CController {
 
 	protected function checkInput() {
 		$fields = [
-			'usrgrpid'        => 'required|db usrgrp.usrgrpid',
-			'name'            => 'not_empty|db usrgrp.name',
-			'userids'         => 'array_db users.userid',
-			'gui_access'      => 'db usrgrp.gui_access|in '.implode(',', [GROUP_GUI_ACCESS_SYSTEM, GROUP_GUI_ACCESS_INTERNAL, GROUP_GUI_ACCESS_LDAP, GROUP_GUI_ACCESS_DISABLED]),
-			'users_status'    => 'db usrgrp.users_status|in '.GROUP_STATUS_ENABLED.','.GROUP_STATUS_DISABLED,
-			'debug_mode'      => 'db usrgrp.debug_mode|in '.GROUP_DEBUG_MODE_ENABLED.','.GROUP_DEBUG_MODE_DISABLED,
+			'usrgrpid' =>			'required|db usrgrp.usrgrpid',
+			'name' =>				'not_empty|db usrgrp.name',
+			'userids' =>			'array_db users.userid',
+			'gui_access' =>			'db usrgrp.gui_access|in '.implode(',', [GROUP_GUI_ACCESS_SYSTEM, GROUP_GUI_ACCESS_INTERNAL, GROUP_GUI_ACCESS_LDAP, GROUP_GUI_ACCESS_DISABLED]),
+			'userdirectoryid' =>	'db usrgrp.userdirectoryid',
+			'users_status' =>		'db usrgrp.users_status|in '.GROUP_STATUS_ENABLED.','.GROUP_STATUS_DISABLED,
+			'debug_mode' =>			'db usrgrp.debug_mode|in '.GROUP_DEBUG_MODE_ENABLED.','.GROUP_DEBUG_MODE_DISABLED,
 
-			'group_rights'    => 'array',
-			'tag_filters'     => 'array',
+			'group_rights' =>		'array',
+			'tag_filters' =>		'array',
 
-			'new_group_right' => 'array',
-			'new_tag_filter'  => 'array',
+			'new_group_right' =>	'array',
+			'new_tag_filter' =>		'array',
 
-			'form_refresh'    => 'int32'
+			'form_refresh' =>		'int32'
 		];
 
 		$ret = $this->validateInput($fields);
@@ -74,7 +75,9 @@ class CControllerUsergroupUpdate extends CController {
 			'rights' => []
 		];
 
-		$this->getInputs($user_group, ['usrgrpid', 'name', 'users_status', 'gui_access', 'debug_mode']);
+		$this->getInputs($user_group, ['usrgrpid', 'name', 'users_status', 'gui_access', 'debug_mode',
+			'userdirectoryid'
+		]);
 
 		$group_rights = applyHostGroupRights($this->getInput('group_rights', []));
 
diff --git a/ui/app/views/administration.authentication.edit.php b/ui/app/views/administration.authentication.edit.php
index 1f6d1385d7a..1e0413cf939 100644
--- a/ui/app/views/administration.authentication.edit.php
+++ b/ui/app/views/administration.authentication.edit.php
@@ -21,6 +21,7 @@
 
 /**
  * @var CView $this
+ * @var array $data
  */
 
 $this->includeJsFile('administration.authentication.edit.js.php');
@@ -134,24 +135,6 @@ $http_tab = (new CFormList('list_http'))
 			->setUncheckedValue(ZBX_AUTH_CASE_INSENSITIVE)
 	);
 
-// LDAP configuration fields.
-if ($data['change_bind_password']) {
-	$password_box = [
-		new CVar('change_bind_password', 1),
-		(new CPassBox('ldap_bind_password', $data['ldap_bind_password']))
-			->setEnabled($data['ldap_enabled'])
-			->setWidth(ZBX_TEXTAREA_SMALL_WIDTH)
-	];
-}
-else {
-	$password_box = [
-		new CVar('action_passw_change', $data['action_passw_change']),
-		(new CButton('change_bind_password', _('Change password')))
-			->setEnabled($data['ldap_enabled'])
-			->addClass(ZBX_STYLE_BTN_GREY)
-	];
-}
-
 $ldap_tab = (new CFormList('list_ldap'))
 	->addRow(new CLabel(_('Enable LDAP authentication'), 'ldap_configured'),
 		$data['ldap_error']
@@ -160,54 +143,38 @@ $ldap_tab = (new CFormList('list_ldap'))
 				->setChecked($data['ldap_configured'] == ZBX_AUTH_LDAP_ENABLED)
 				->setUncheckedValue(ZBX_AUTH_LDAP_DISABLED)
 	)
-	->addRow((new CLabel(_('LDAP host'), 'ldap_host'))->setAsteriskMark(),
-		(new CTextBox('ldap_host', $data['ldap_host']))
-			->setEnabled($data['ldap_enabled'])
-			->setWidth(ZBX_TEXTAREA_STANDARD_WIDTH)
-			->setAriaRequired()
-	)
-	->addRow((new CLabel(_('Port'), 'ldap_port'))->setAsteriskMark(),
-		(new CNumericBox('ldap_port', $data['ldap_port'], 5))
-			->setEnabled($data['ldap_enabled'])
-			->setWidth(ZBX_TEXTAREA_NUMERIC_STANDARD_WIDTH)
-			->setAriaRequired()
-	)
-	->addRow((new CLabel(_('Base DN'), 'ldap_base_dn'))->setAsteriskMark(),
-		(new CTextBox('ldap_base_dn', $data['ldap_base_dn']))
-			->setEnabled($data['ldap_enabled'])
-			->setWidth(ZBX_TEXTAREA_STANDARD_WIDTH)
-			->setAriaRequired()
-	)
-	->addRow((new CLabel(_('Search attribute'), 'ldap_search_attribute'))->setAsteriskMark(),
-		(new CTextBox('ldap_search_attribute', $data['ldap_search_attribute']))
-			->setEnabled($data['ldap_enabled'])
-			->setWidth(ZBX_TEXTAREA_STANDARD_WIDTH)
-			->setAriaRequired()
-	)
-	->addRow(new CLabel(_('Bind DN'), 'ldap_bind_dn'),
-		(new CTextBox('ldap_bind_dn', $data['ldap_bind_dn']))
-			->setEnabled($data['ldap_enabled'])
-			->setWidth(ZBX_TEXTAREA_STANDARD_WIDTH)
+	->addRow((new CLabel(_('Servers')))->setAsteriskMark(),
+		(new CDiv(
+			(new CTable())
+				->setId('ldap-servers')
+				->setHeader(
+					(new CRowHeader([
+						(new CColHeader(_('Name')))->addStyle('width: 40%'),
+						(new CColHeader(_('Host')))->addStyle('width: 40%'),
+						(new CColHeader(_('User groups')))->addClass(ZBX_STYLE_NOWRAP),
+						_('Default'),
+						''
+					]))->addClass(ZBX_STYLE_GREY)
+				)
+				->addItem(
+					(new CTag('tfoot', true))
+						->addItem(
+							(new CCol(
+								(new CSimpleButton(_('Add')))
+									->addClass(ZBX_STYLE_BTN_LINK)
+									->addClass('js-add')
+							))->setColSpan(5)
+						)
+				)
+				->addStyle('width: 100%;')
+		))
+			->addClass(ZBX_STYLE_TABLE_FORMS_SEPARATOR)
+			->addStyle('min-width: '.ZBX_TEXTAREA_BIG_WIDTH.'px;')
 	)
 	->addRow(new CLabel(_('Case-sensitive login'), 'ldap_case_sensitive'),
 		(new CCheckBox('ldap_case_sensitive', ZBX_AUTH_CASE_SENSITIVE))
 			->setChecked($data['ldap_case_sensitive'] == ZBX_AUTH_CASE_SENSITIVE)
-			->setEnabled($data['ldap_configured'] == ZBX_AUTH_LDAP_ENABLED)
 			->setUncheckedValue(ZBX_AUTH_CASE_INSENSITIVE)
-	)
-	->addRow(new CLabel(_('Bind password'), 'ldap_bind_password'), $password_box)
-	->addRow(_('Test authentication'), ' ['._('must be a valid LDAP user').']')
-	->addRow((new CLabel(_('Login'), 'ldap_test_user'))->setAsteriskMark(),
-		(new CTextBox('ldap_test_user', $data['ldap_test_user']))
-			->setEnabled($data['ldap_enabled'])
-			->setWidth(ZBX_TEXTAREA_STANDARD_WIDTH)
-			->setAriaRequired()
-	)
-	->addRow((new CLabel(_('User password'), 'ldap_test_password'))->setAsteriskMark(),
-		(new CPassBox('ldap_test_password', $data['ldap_test_password']))
-			->setEnabled($data['ldap_enabled'])
-			->setWidth(ZBX_TEXTAREA_SMALL_WIDTH)
-			->setAriaRequired()
 	);
 
 // SAML authentication fields.
@@ -323,9 +290,8 @@ $saml_tab = (new CFormList('list_saml'))
 	->setTitle(_('Authentication'))
 	->addItem((new CForm())
 		->addVar('action', $data['action_submit'])
-		->addVar('db_authentication_type', $data['db_authentication_type'])
+		->addVar('ldap_removed_userdirectoryids', $data['ldap_removed_userdirectoryids'])
 		->setId('authentication-form')
-		->setName('form_auth')
 		->setAttribute('aria-labeledby', ZBX_STYLE_PAGE_TITLE)
 		->disablePasswordAutofill()
 		->addItem((new CTabView())
@@ -335,12 +301,18 @@ $saml_tab = (new CFormList('list_saml'))
 			->addTab('ldap', _('LDAP settings'), $ldap_tab, TAB_INDICATOR_AUTH_LDAP)
 			->addTab('saml', _('SAML settings'), $saml_tab, TAB_INDICATOR_AUTH_SAML)
 			->setFooter(makeFormFooter(
-				(new CSubmit('update', _('Update'))),
-				[(new CSubmitButton(_('Test'), 'ldap_test', 1))
-					->addStyle(($data['form_refresh'] && CCookieHelper::get('tab') == 2) ? '' : 'display: none')
-					->setEnabled($data['ldap_enabled'])
-				]
+				(new CSubmit('update', _('Update')))
 			))
 			->onTabChange('jQuery("[name=ldap_test]")[(ui.newTab.index() == 2) ? "show" : "hide"]()')
 	))
 	->show();
+
+(new CScriptTag(
+	'view.init('. json_encode([
+		'ldap_servers' => $data['ldap_servers'],
+		'ldap_default_row_index' => $data['ldap_default_row_index'],
+		'db_authentication_type' => $data['db_authentication_type']
+	], JSON_FORCE_OBJECT).');'
+))
+	->setOnDocumentReady()
+	->show();
diff --git a/ui/app/views/administration.usergroup.edit.php b/ui/app/views/administration.usergroup.edit.php
index 1685674ff97..f0a946db958 100644
--- a/ui/app/views/administration.usergroup.edit.php
+++ b/ui/app/views/administration.usergroup.edit.php
@@ -74,8 +74,16 @@ if ($data['can_update_group']) {
 			GROUP_GUI_ACCESS_DISABLED => user_auth_type2str(GROUP_GUI_ACCESS_DISABLED)
 		]));
 
+	$userdirectory = (new CSelect('userdirectoryid'))
+		->setValue($data['userdirectoryid'])
+		->setFocusableElementId('userdirectoryid')
+		->addOption((new CSelectOption(0, _('Default')))->addClass(ZBX_STYLE_DEFAULT_OPTION))
+		->addOptions(CSelect::createOptionsFromArray($data['userdirectories']))
+		->setAdaptiveWidth(ZBX_TEXTAREA_STANDARD_WIDTH);
+
 	$form_list
 		->addRow((new CLabel(_('Frontend access'), $select_gui_access->getFocusableElementId())), $select_gui_access)
+		->addRow((new CLabel(_('LDAP Server'), $userdirectory->getFocusableElementId())), $userdirectory)
 		->addRow(_('Enabled'), (new CCheckBox('users_status', GROUP_STATUS_ENABLED))
 			->setUncheckedValue(GROUP_STATUS_DISABLED)
 			->setChecked($data['users_status'] == GROUP_STATUS_ENABLED)
diff --git a/ui/app/views/js/administration.authentication.edit.js.php b/ui/app/views/js/administration.authentication.edit.js.php
index 1baac8907f4..ba11431449c 100644
--- a/ui/app/views/js/administration.authentication.edit.js.php
+++ b/ui/app/views/js/administration.authentication.edit.js.php
@@ -24,53 +24,227 @@
  */
 ?>
 
-<script type="text/javascript">
-	jQuery(function($) {
-		var $form = $('[name=form_auth]'),
-			warn = true;
-
-		$form.submit(function() {
-			var proceed = !warn
-				|| $('[name=authentication_type]:checked').val() == $('[name=db_authentication_type]').val()
-				|| confirm(<?= json_encode(
-					_('Switching authentication method will reset all except this session! Continue?')
-				) ?>);
-			warn = true;
-
-			$form.trimValues(['#saml_idp_entityid', '#saml_sso_url', '#saml_slo_url', '#saml_username_attribute',
+<script>
+	const view = new class {
+
+		constructor() {
+			this.form = null;
+			this.db_authentication_type = null;
+		}
+
+		init({ldap_servers, ldap_default_row_index, db_authentication_type}) {
+			this.form = document.getElementById('authentication-form');
+			this.db_authentication_type = db_authentication_type;
+
+			this._addEventListeners();
+			this._addLdapServers(ldap_servers, ldap_default_row_index);
+		}
+
+		_addEventListeners() {
+			document
+				.getElementById('ldap-servers')
+				.addEventListener('click', (e) => {
+					if (e.target.classList.contains('js-add')) {
+						this.editLdapServer();
+					}
+					else if (e.target.classList.contains('js-edit')) {
+						this.editLdapServer(e.target.closest('tr'));
+					}
+					else if (e.target.classList.contains('js-remove')) {
+						const table = e.target.closest('table');
+						const userdirectoryid_input = e.target.closest('tr')
+							.querySelector('input[name$="[userdirectoryid]"]');
+
+						if (userdirectoryid_input !== null) {
+							const input = document.createElement('input');
+							input.type = 'hidden';
+							input.name = 'ldap_removed_userdirectoryids[]';
+							input.value = userdirectoryid_input.value;
+							this.form.appendChild(input);
+						}
+
+						e.target.closest('tr').remove();
+
+						if (table.querySelector('input[name="ldap_default_row_index"]:checked') === null) {
+							const default_ldap = table.querySelector('input[name="ldap_default_row_index"]');
+
+							if (default_ldap !== null) {
+								default_ldap.checked = true;
+							}
+						}
+					}
+				});
+
+			document.getElementById('http_auth_enabled').addEventListener('change', (e) => {
+				this.form.querySelectorAll('[name^=http_]').forEach(field => {
+					if (!field.isSameNode(e.target)) {
+						field.disabled = !e.target.checked;
+					}
+				});
+			});
+
+			document.getElementById('saml_auth_enabled').addEventListener('change', (e) => {
+				this.form.querySelectorAll('[name^=saml_]').forEach(field => {
+					if (!field.isSameNode(e.target)) {
+						field.disabled = !e.target.checked;
+					}
+				});
+			});
+
+			this.form.addEventListener('submit', (e) => {
+				if (!this._authFormSubmit()) {
+					e.preventDefault();
+				}
+			});
+		}
+
+		_authFormSubmit() {
+			const fields_to_trim = ['#saml_idp_entityid', '#saml_sso_url', '#saml_slo_url', '#saml_username_attribute',
 				'#saml_sp_entityid', '#saml_nameid_format'
-			]);
+			];
+			document.querySelectorAll(fields_to_trim.join(', ')).forEach((elem) => {
+				elem.value = elem.value.trim();
+			});
 
-			return proceed;
-		});
+			const auth_type = document.querySelector('[name=authentication_type]:checked').value;
+			const warning_msg = <?= json_encode(
+				_('Switching authentication method will reset all except this session! Continue?')
+			) ?>;
 
-		$form.find('#http_auth_enabled, #ldap_configured, #saml_auth_enabled').on('change', function() {
-			var fields;
+			return (auth_type == this.db_authentication_type || confirm(warning_msg));
+		}
 
-			if ($(this).is('#http_auth_enabled')) {
-				fields = $form.find('[name^=http_]');
+		_addLdapServers(ldap_servers, ldap_default_row_index) {
+			for (const [row_index, ldap] of Object.entries(ldap_servers)) {
+				ldap.row_index = row_index;
+				ldap.is_default = (ldap.row_index == ldap_default_row_index) ? 'checked' : '';
+
+				document
+						.querySelector('#ldap-servers tbody')
+						.appendChild(this._prepareServerRow(ldap));
 			}
-			else if ($(this).is('#ldap_configured')) {
-				fields = $form.find('[name^=ldap_],button[name=change_bind_password]');
+		}
+
+		editLdapServer(row = null) {
+			let popup_params;
+
+			if (row !== null) {
+				const row_index = row.dataset.row_index;
+
+				popup_params = {
+					row_index,
+					add_ldap_server: 0,
+					name: row.querySelector(`[name="ldap_servers[${row_index}][name]"`).value,
+					host: row.querySelector(`[name="ldap_servers[${row_index}][host]"`).value,
+					port: row.querySelector(`[name="ldap_servers[${row_index}][port]"`).value,
+					base_dn: row.querySelector(`[name="ldap_servers[${row_index}][base_dn]"`).value,
+					search_attribute: row.querySelector(`[name="ldap_servers[${row_index}][search_attribute]"`).value,
+					search_filter: row.querySelector(`[name="ldap_servers[${row_index}][search_filter]"`).value,
+					start_tls: row.querySelector(`[name="ldap_servers[${row_index}][start_tls]"`).value,
+					bind_dn: row.querySelector(`[name="ldap_servers[${row_index}][bind_dn]"`).value,
+					description: row.querySelector(`[name="ldap_servers[${row_index}][description]"`).value
+				};
+
+				const userdirectoryid_input = row.querySelector(`[name="ldap_servers[${row_index}][userdirectoryid]"`);
+				const bind_password_input = row.querySelector(`[name="ldap_servers[${row_index}][bind_password]"`);
+
+				if (userdirectoryid_input !== null) {
+					popup_params['userdirectoryid'] = userdirectoryid_input.value;
+				}
+
+				if (bind_password_input !== null) {
+					popup_params['bind_password'] = bind_password_input.value;
+				}
 			}
 			else {
-				fields = $form.find('[name^=saml_]');
+				let row_index = 0;
+
+				while (document.querySelector(`#ldap-servers [data-row_index="${row_index}"]`) !== null) {
+					row_index++;
+				}
+
+				popup_params = {
+					row_index,
+					add_ldap_server: 1
+				};
 			}
 
-			fields
-				.not('[name=http_auth_enabled], [name=ldap_configured], [name=saml_auth_enabled]')
-				.prop('disabled', !this.checked);
-		});
+			const overlay = PopUp('popup.ldap.edit', popup_params, {dialogueid: 'ldap_edit'});
+
+			overlay.$dialogue[0].addEventListener('dialogue.submit', (e) => {
+				const ldap = e.detail;
+
+				if (row === null) {
+					ldap.is_default = document.getElementById('ldap-servers')
+							.querySelector('input[name="ldap_default_row_index"]:checked') === null
+						? 'checked'
+						: '';
+					ldap.usrgrps = 0;
 
-		$form.find('button#change_bind_password').click(function() {
-			$form.find('[name=action]')
-				.val($form.find('[name=action_passw_change]').val());
+					document
+						.querySelector('#ldap-servers tbody')
+						.appendChild(this._prepareServerRow(ldap));
+				}
+				else {
+					ldap.is_default = row.querySelector('input[name="ldap_default_row_index"]').checked === true
+						? 'checked'
+						: '';
+					ldap.usrgrps = row.querySelector('.js-ldap-usergroups').textContent;
+
+					row.parentNode.insertBefore(this._prepareServerRow(ldap), row);
+					row.remove();
+				}
+			});
+		}
+
+		_prepareServerRow(ldap) {
+			const template_ldap_server_row = new Template(this._templateLdapServerRow());
+			const template = document.createElement('template');
+			template.innerHTML = template_ldap_server_row.evaluate(ldap).trim();
+			const row = template.content.firstChild;
+
+			const optional_fields = ['userdirectoryid', 'bind_password', 'start_tls', 'search_filter'];
+
+			for (const field of optional_fields) {
+				if (!(field in ldap)) {
+					row.querySelector('input[name="ldap_servers[' + ldap.row_index + '][' + field + ']"]').remove();
+				}
+			}
+
+			if (ldap.usrgrps > 0) {
+				row.querySelector('.js-remove').disabled = true;
+			}
 
-			submitFormWithParam('form_auth', 'change_bind_password', '1');
-		});
+			return row;
+		}
 
-		$form.find('[name=ldap_test]').click(function() {
-			warn = false;
-		});
-	});
+		_templateLdapServerRow() {
+			return `
+				<tr data-row_index="#{row_index}">
+					<td>
+						<a href="javascript:void(0);" class="wordwrap js-edit">#{name}</a>
+						<input type="hidden" name="ldap_servers[#{row_index}][userdirectoryid]" value="#{userdirectoryid}">
+						<input type="hidden" name="ldap_servers[#{row_index}][name]" value="#{name}">
+						<input type="hidden" name="ldap_servers[#{row_index}][host]" value="#{host}">
+						<input type="hidden" name="ldap_servers[#{row_index}][port]" value="#{port}">
+						<input type="hidden" name="ldap_servers[#{row_index}][base_dn]" value="#{base_dn}">
+						<input type="hidden" name="ldap_servers[#{row_index}][search_attribute]" value="#{search_attribute}">
+						<input type="hidden" name="ldap_servers[#{row_index}][search_filter]" value="#{search_filter}">
+						<input type="hidden" name="ldap_servers[#{row_index}][start_tls]" value="#{start_tls}">
+						<input type="hidden" name="ldap_servers[#{row_index}][bind_dn]" value="#{bind_dn}">
+						<input type="hidden" name="ldap_servers[#{row_index}][bind_password]" value="#{bind_password}">
+						<input type="hidden" name="ldap_servers[#{row_index}][description]" value="#{description}">
+					</td>
+					<td class="wordbreak">#{host}</td>
+					<td class="js-ldap-usergroups">#{usrgrps}</td>
+					<td>
+						<input type="radio" name="ldap_default_row_index" value="#{row_index}" #{is_default}>
+					</td>
+					<td>
+						<button type="button" class="<?= ZBX_STYLE_BTN_LINK ?> js-remove"><?= _('Remove') ?></button>
+					</td>
+				</tr>
+			`;
+		}
+	};
 </script>
diff --git a/ui/app/views/js/administration.usergroup.edit.js.php b/ui/app/views/js/administration.usergroup.edit.js.php
index 0cfade23d00..009ad408762 100644
--- a/ui/app/views/js/administration.usergroup.edit.js.php
+++ b/ui/app/views/js/administration.usergroup.edit.js.php
@@ -34,15 +34,34 @@
 			$tag_filter_table_container  = $form.find('table#tag-filter-table').parent(),
 			$ms_tag_filter_groups        = $new_tag_filter_table.find('.multiselect'),
 			$ms_group_right_groups       = $new_group_right_table.find('.multiselect'),
+			$userdirectory               = $form.find('[name="userdirectoryid"]'),
+			$gui_access                  = $form.find('[name="gui_access"]'),
 			timeoutid_new_group_right,
 			timeoutid_new_tag_filter,
 			xhr_new_group_right,
 			xhr_new_tag_filter;
 
+		$gui_access.on('change', onFrontendAccessChange);
+		onFrontendAccessChange.apply($gui_access);
+
 		$form.submit(function() {
 			$form.trimValues(['#name']);
 		});
 
+		/**
+		 * Handle "Frontend access" selector change.
+		 */
+		function onFrontendAccessChange() {
+			let gui_access = $(this).val();
+
+			if (gui_access == <?= GROUP_GUI_ACCESS_INTERNAL ?> || gui_access == <?= GROUP_GUI_ACCESS_DISABLED ?>) {
+				$userdirectory.attr('disabled', 'disabled');
+			}
+			else {
+				$userdirectory.removeAttr('disabled');
+			}
+		}
+
 		/**
 		 * Collects data.
 		 *
diff --git a/ui/app/views/js/popup.ldap.edit.js.php b/ui/app/views/js/popup.ldap.edit.js.php
new file mode 100644
index 00000000000..37a79406725
--- /dev/null
+++ b/ui/app/views/js/popup.ldap.edit.js.php
@@ -0,0 +1,166 @@
+<?php declare(strict_types = 1);
+/*
+** Zabbix
+** Copyright (C) 2001-2022 Zabbix SIA
+**
+** This program is free software; you can redistribute it and/or modify
+** it under the terms of the GNU General Public License as published by
+** the Free Software Foundation; either version 2 of the License, or
+** (at your option) any later version.
+**
+** This program is distributed in the hope that it will be useful,
+** but WITHOUT ANY WARRANTY; without even the implied warranty of
+** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+** GNU General Public License for more details.
+**
+** You should have received a copy of the GNU General Public License
+** along with this program; if not, write to the Free Software
+** Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+**/
+
+
+?>
+
+window.ldap_edit_popup = new class {
+
+	constructor() {
+		this.overlay = null;
+		this.dialogue = null;
+		this.form = null;
+		this.advanced_chbox = null;
+	}
+
+	init() {
+		this.overlay = overlays_stack.getById('ldap_edit');
+		this.dialogue = this.overlay.$dialogue[0];
+		this.form = this.overlay.$dialogue.$body[0].querySelector('form');
+		this.advanced_chbox = document.getElementById('advanced_configuration');
+
+		this.advanced_chbox.addEventListener('change', (e) => {
+			this.toggleAdvancedConfiguration(e.target.checked);
+		});
+
+		this.toggleAdvancedConfiguration(this.advanced_chbox.checked);
+
+		if (document.getElementById('bind-password-btn') !== null) {
+			document.getElementById('bind-password-btn').addEventListener('click', this.showPasswordField);
+		}
+	}
+
+	toggleAdvancedConfiguration(checked) {
+		for (const element of this.form.querySelectorAll('.advanced-configuration')) {
+			element.classList.toggle('<?= ZBX_STYLE_DISPLAY_NONE ?>', !checked);
+		}
+	}
+
+	showPasswordField(e) {
+		const form_field = e.target.parentNode;
+		const password_field = form_field.querySelector('[name="bind_password"][type="password"]');
+		const password_var = form_field.querySelector('[name="bind_password"][type="hidden"]');
+
+		password_field.style.display = '';
+		password_field.disabled = false;
+
+		if (password_var !== null) {
+			form_field.removeChild(password_var);
+		}
+		form_field.removeChild(e.target);
+	}
+
+	openTestPopup() {
+		const fields = this.preprocessFormFields(getFormFields(this.form));
+
+		const popup_params = {
+			host: fields.host,
+			port: fields.port,
+			base_dn: fields.base_dn,
+			bind_dn: fields.bind_dn,
+			search_attribute: fields.search_attribute
+		};
+
+		const optional_fields = ['userdirectoryid', 'bind_password', 'start_tls', 'search_filter'];
+
+		for (const field of optional_fields) {
+			if (fields[field] !== undefined) {
+				popup_params[field] = fields[field];
+			}
+		}
+
+		const test_overlay = PopUp('popup.ldap.test.edit', popup_params, {dialogueid: 'ldap_test_edit'});
+		test_overlay.xhr.then(() => this.overlay.unsetLoading());
+	}
+
+	submit() {
+		this.removePopupMessages();
+		this.overlay.setLoading();
+
+		const fields = this.preprocessFormFields(getFormFields(this.form));
+		const curl = new Curl(this.form.getAttribute('action'), false);
+
+		fetch(curl.getUrl(), {
+			method: 'POST',
+			headers: {'Content-Type': 'application/json'},
+			body: JSON.stringify(fields)
+		})
+			.then((response) => response.json())
+			.then((response) => {
+				if ('error' in response) {
+					throw {error: response.error};
+				}
+
+				overlayDialogueDestroy(this.overlay.dialogueid);
+
+				this.dialogue.dispatchEvent(new CustomEvent('dialogue.submit', {detail: response.body}));
+			})
+			.catch((exception) => {
+				let title;
+				let messages = [];
+
+				if (typeof exception === 'object' && 'error' in exception) {
+					title = exception.error.title;
+					messages = exception.error.messages;
+				}
+				else {
+					title = <?= json_encode(_('Unexpected server error.')) ?>;
+				}
+
+				const message_box = makeMessageBox('bad', messages, title, true, true)[0];
+
+				this.form.parentNode.insertBefore(message_box, this.form);
+			})
+			.finally(() => {
+				this.overlay.unsetLoading();
+			});
+	}
+
+	removePopupMessages() {
+		for (const el of this.form.parentNode.children) {
+			if (el.matches('.msg-good, .msg-bad, .msg-warning')) {
+				el.parentNode.removeChild(el);
+			}
+		}
+	}
+
+	preprocessFormFields(fields) {
+		this.trimFields(fields);
+
+		if (fields.advanced_configuration != 1) {
+			delete fields.start_tls;
+			delete fields.search_filter;
+		}
+
+		delete fields.advanced_configuration;
+
+		return fields;
+	}
+
+	trimFields(fields) {
+		const fields_to_trim = ['name', 'host', 'base_dn', 'bind_dn', 'search_attribute', 'search_filter',
+			'description'];
+		for (const field of fields_to_trim) {
+			if (field in fields) {
+				fields[field] = fields[field].trim();
+			}
+		}
+	}
+}();
diff --git a/ui/app/views/js/popup.ldap.test.edit.js.php b/ui/app/views/js/popup.ldap.test.edit.js.php
new file mode 100644
index 00000000000..85775213789
--- /dev/null
+++ b/ui/app/views/js/popup.ldap.test.edit.js.php
@@ -0,0 +1,98 @@
+<?php declare(strict_types = 1);
+/*
+** Zabbix
+** Copyright (C) 2001-2022 Zabbix SIA
+**
+** This program is free software; you can redistribute it and/or modify
+** it under the terms of the GNU General Public License as published by
+** the Free Software Foundation; either version 2 of the License, or
+** (at your option) any later version.
+**
+** This program is distributed in the hope that it will be useful,
+** but WITHOUT ANY WARRANTY; without even the implied warranty of
+** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+** GNU General Public License for more details.
+**
+** You should have received a copy of the GNU General Public License
+** along with this program; if not, write to the Free Software
+** Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+**/
+
+
+/**
+ * @var CView $this
+ */
+?>
+
+window.ldap_test_edit_popup = new class {
+
+	constructor() {
+		this.overlay = null;
+		this.dialogue = null;
+		this.form = null;
+	}
+
+	init() {
+		this.overlay = overlays_stack.getById('ldap_test_edit');
+		this.dialogue = this.overlay.$dialogue[0];
+		this.form = this.overlay.$dialogue.$body[0].querySelector('form');
+	}
+
+	submit() {
+		this.removePopupMessages();
+		this.overlay.setLoading();
+
+		const fields = this.trimFields(getFormFields(this.form));
+		const curl = new Curl(this.form.getAttribute('action'), false);
+
+		fetch(curl.getUrl(), {
+			method: 'POST',
+			headers: {'Content-Type': 'application/json'},
+			body: JSON.stringify(fields)
+		})
+			.then((response) => response.json())
+			.then((response) => {
+				if ('error' in response) {
+					throw {error: response.error};
+				}
+				else if ('success' in response) {
+					const message_box = makeMessageBox('good', [], response.success.title, false, true)[0];
+
+					this.form.parentNode.insertBefore(message_box, this.form);
+				}
+			})
+			.catch((exception) => {
+				let title;
+				let messages = [];
+
+				if (typeof exception === 'object' && 'error' in exception) {
+					title = exception.error.title;
+					messages = exception.error.messages;
+				}
+				else {
+					title = <?= json_encode(_('Unexpected server error.')) ?>;
+				}
+
+				const message_box = makeMessageBox('bad', messages, title, true, true)[0];
+
+				this.form.parentNode.insertBefore(message_box, this.form);
+			})
+			.finally(() => {
+				this.overlay.unsetLoading();
+			});
+	}
+
+	removePopupMessages() {
+		for (const el of this.form.parentNode.children) {
+			if (el.matches('.msg-good, .msg-bad, .msg-warning')) {
+				el.parentNode.removeChild(el);
+			}
+		}
+	}
+
+	trimFields(fields) {
+		fields.test_username = fields.test_username.trim();
+
+		return fields;
+	}
+};
diff --git a/ui/app/views/popup.ldap.edit.php b/ui/app/views/popup.ldap.edit.php
new file mode 100644
index 00000000000..51b85ad6c30
--- /dev/null
+++ b/ui/app/views/popup.ldap.edit.php
@@ -0,0 +1,190 @@
+<?php declare(strict_types = 1);
+/*
+** Zabbix
+** Copyright (C) 2001-2022 Zabbix SIA
+**
+** This program is free software; you can redistribute it and/or modify
+** it under the terms of the GNU General Public License as published by
+** the Free Software Foundation; either version 2 of the License, or
+** (at your option) any later version.
+**
+** This program is distributed in the hope that it will be useful,
+** but WITHOUT ANY WARRANTY; without even the implied warranty of
+** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+** GNU General Public License for more details.
+**
+** You should have received a copy of the GNU General Public License
+** along with this program; if not, write to the Free Software
+** Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+**/
+
+
+/**
+ * @var CView $this
+ * @var array $data
+ */
+
+$form_action = (new CUrl('zabbix.php'))
+	->setArgument('action', 'popup.ldap.check')
+	->getUrl();
+
+$form = (new CForm('post', $form_action))
+	->addItem(getMessages())
+	->addItem((new CInput('submit'))->addStyle('display: none;'))
+	->addVar('row_index', $data['row_index'])
+	->addVar('userdirectoryid', $data['userdirectoryid'] ?? null)
+	->addItem((new CFormGrid())
+		->addItem([
+			(new CLabel(_('Name'), 'name'))->setAsteriskMark(),
+			new CFormField(
+				(new CTextBox('name', $data['name'], false, DB::getFieldLength('userdirectory', 'name')))
+					->setWidth(ZBX_TEXTAREA_STANDARD_WIDTH)
+					->setAriaRequired()
+					->setAttribute('autofocus', 'autofocus')
+			)
+		])
+		->addItem([
+			(new CLabel(_('Host'), 'host'))->setAsteriskMark(),
+			new CFormField(
+				(new CTextBox('host', $data['host'], false, DB::getFieldLength('userdirectory', 'host')))
+					->setWidth(ZBX_TEXTAREA_STANDARD_WIDTH)
+					->setAriaRequired()
+			)
+		])
+		->addItem([
+			(new CLabel(_('Port'), 'port'))->setAsteriskMark(),
+			new CFormField(
+				(new CNumericBox('port', $data['port'], 5))
+					->setWidth(ZBX_TEXTAREA_NUMERIC_STANDARD_WIDTH)
+					->setAriaRequired()
+			)
+		])
+		->addItem([
+			(new CLabel(_('Base DN'), 'base_dn'))->setAsteriskMark(),
+			new CFormField(
+				(new CTextBox('base_dn', $data['base_dn'], false, DB::getFieldLength('userdirectory', 'base_dn')))
+					->setWidth(ZBX_TEXTAREA_STANDARD_WIDTH)
+					->setAriaRequired()
+			)
+		])
+		->addItem([
+			(new CLabel(_('Search attribute'), 'search_attribute'))->setAsteriskMark(),
+			new CFormField(
+				(new CTextBox('search_attribute', $data['search_attribute'], false,
+					DB::getFieldLength('userdirectory', 'search_attribute')
+				))
+					->setWidth(ZBX_TEXTAREA_STANDARD_WIDTH)
+					->setAriaRequired()
+			)
+		])
+		->addItem([
+			new CLabel(_('Bind DN'), 'bind_dn'),
+			new CFormField(
+				(new CTextBox('bind_dn', $data['bind_dn'], false, DB::getFieldLength('userdirectory', 'bind_dn')))
+					->setWidth(ZBX_TEXTAREA_STANDARD_WIDTH)
+			)
+		])
+		->addItem([
+			new CLabel(_('Bind password'), 'bind_password'),
+			new CFormField($data['add_ldap_server'] == 0
+				? [
+					array_key_exists('bind_password', $data)
+						? new CVar('bind_password', $data['bind_password'])
+						: null,
+					(new CSimpleButton(_('Change password')))
+						->addClass(ZBX_STYLE_BTN_GREY)
+						->setId('bind-password-btn'),
+					(new CPassBox('bind_password', '', DB::getFieldLength('userdirectory', 'bind_password')))
+						->setWidth(ZBX_TEXTAREA_SMALL_WIDTH)
+						->addStyle('display: none;')
+						->setAttribute('disabled', 'disabled')
+				]
+				: (new CPassBox('bind_password', '', DB::getFieldLength('userdirectory', 'bind_password')))
+					->setWidth(ZBX_TEXTAREA_SMALL_WIDTH)
+			)
+		])
+		->addItem([
+			new CLabel(_('Description'), 'description'),
+			new CFormField(
+				(new CTextArea('description', $data['description'], ['rows' => 3]))
+					->setWidth(ZBX_TEXTAREA_STANDARD_WIDTH)
+					->setMaxlength(DB::getFieldLength('userdirectory', 'description'))
+			)
+		])
+		->addItem([
+			new CLabel(_('Advanced configuration'), 'advanced_configuration'),
+			new CFormField(
+				(new CCheckBox('advanced_configuration'))->setChecked($data['advanced_configuration'])
+			)
+		])
+		->addItem([
+			(new CLabel(_('StartTLS'), 'start_tls'))->addClass('advanced-configuration'),
+			(new CFormField(
+				(new CCheckBox('start_tls', ZBX_AUTH_START_TLS_ON))
+					->setChecked($data['start_tls'] == ZBX_AUTH_START_TLS_ON)
+			))->addClass('advanced-configuration')
+		])
+		->addItem([
+			(new CLabel(_('Search filter'), 'search_filter'))->addClass('advanced-configuration'),
+			(new CFormField(
+				(new CTextBox('search_filter', $data['search_filter'], false,
+					DB::getFieldLength('userdirectory', 'search_filter')
+				))
+					->setAttribute('placeholder', '(%{attr}=%{user})')
+					->setWidth(ZBX_TEXTAREA_STANDARD_WIDTH)
+			))->addClass('advanced-configuration')
+		])
+	)
+	->addItem(
+		(new CScriptTag('
+			ldap_edit_popup.init();
+		'))->setOnDocumentReady()
+	);
+
+if ($data['add_ldap_server']) {
+	$title = _('New LDAP server');
+	$buttons = [
+		[
+			'title' => _('Add'),
+			'class' => 'js-add',
+			'keepOpen' => true,
+			'isSubmit' => true,
+			'action' => 'ldap_edit_popup.submit();'
+		]
+	];
+}
+else {
+	$title = _('LDAP Server');
+	$buttons = [
+		[
+			'title' => _('Update'),
+			'class' => 'js-update',
+			'keepOpen' => true,
+			'isSubmit' => true,
+			'action' => 'ldap_edit_popup.submit();'
+		]
+	];
+}
+
+$buttons[] = [
+	'title' => _('Test'),
+	'class' => implode(' ', [ZBX_STYLE_BTN_ALT, 'js-test']),
+	'keepOpen' => true,
+	'isSubmit' => false,
+	'action' => 'ldap_edit_popup.openTestPopup();'
+];
+
+$output = [
+	'header' => $title,
+	'body' => $form->toString(),
+	'buttons' => $buttons,
+	'script_inline' => getPagePostJs().
+		$this->readJsFile('popup.ldap.edit.js.php')
+];
+
+if ($data['user']['debug_mode'] == GROUP_DEBUG_MODE_ENABLED) {
+	CProfiler::getInstance()->stop();
+	$output['debug'] = CProfiler::getInstance()->make()->toString();
+}
+
+echo json_encode($output);
diff --git a/ui/app/views/popup.ldap.test.edit.php b/ui/app/views/popup.ldap.test.edit.php
new file mode 100644
index 00000000000..efbbdb39822
--- /dev/null
+++ b/ui/app/views/popup.ldap.test.edit.php
@@ -0,0 +1,78 @@
+<?php declare(strict_types = 1);
+/*
+** Zabbix
+** Copyright (C) 2001-2022 Zabbix SIA
+**
+** This program is free software; you can redistribute it and/or modify
+** it under the terms of the GNU General Public License as published by
+** the Free Software Foundation; either version 2 of the License, or
+** (at your option) any later version.
+**
+** This program is distributed in the hope that it will be useful,
+** but WITHOUT ANY WARRANTY; without even the implied warranty of
+** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+** GNU General Public License for more details.
+**
+** You should have received a copy of the GNU General Public License
+** along with this program; if not, write to the Free Software
+** Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+**/
+
+
+/**
+ * @var CView $this
+ * @var array $data
+ */
+
+$form_action = (new CUrl('zabbix.php'))
+	->setArgument('action', 'popup.ldap.test.send')
+	->getUrl();
+
+$form = (new CForm('post', $form_action))
+	->addItem((new CInput('submit'))->addStyle('display: none;'))
+	->addItem((new CFormGrid())
+		->addItem([
+			(new CLabel(_('Login'), 'test_username'))->setAsteriskMark(),
+			new CFormField(
+				(new CTextBox('test_username', $data['test_username']))
+					->setWidth(ZBX_TEXTAREA_SMALL_WIDTH)
+					->setAriaRequired()
+			)
+		])
+		->addItem([
+			(new CLabel(_('User password'), 'test_password'))->setAsteriskMark(),
+			new CFormField(
+				(new CPassBox('test_password'))
+					->setWidth(ZBX_TEXTAREA_SMALL_WIDTH)
+					->setAriaRequired()
+			)
+		])
+	)
+	->addItem(
+		(new CScriptTag('ldap_test_edit_popup.init();'))->setOnDocumentReady()
+	);
+
+foreach ($data['ldap_config'] as $field => $value) {
+	$form->addVar($field, $value);
+}
+
+$output = [
+	'header' => _('Test authentication'),
+	'body' => $form->toString(),
+	'buttons' => [
+		[
+			'title' => _('Test'),
+			'keepOpen' => true,
+			'isSubmit' => true,
+			'action' => 'ldap_test_edit_popup.submit();'
+		]
+	],
+	'script_inline' => $this->readJsFile('popup.ldap.test.edit.js.php')
+];
+
+if ($data['user']['debug_mode'] == GROUP_DEBUG_MODE_ENABLED) {
+	CProfiler::getInstance()->stop();
+	$output['debug'] = CProfiler::getInstance()->make()->toString();
+}
+
+echo json_encode($output);
diff --git a/ui/assets/styles/blue-theme.css b/ui/assets/styles/blue-theme.css
index a4941263421..d66cfd8546a 100644
--- a/ui/assets/styles/blue-theme.css
+++ b/ui/assets/styles/blue-theme.css
@@ -2547,6 +2547,9 @@ z-select,
   z-select:focus,
   .z-select:focus {
     outline: none; }
+  z-select ul > li.default-option,
+  .z-select ul > li.default-option {
+    font-style: italic; }
   z-select button.focusable,
   .z-select button.focusable {
     position: relative;
diff --git a/ui/assets/styles/dark-theme.css b/ui/assets/styles/dark-theme.css
index dedc0bd2d4d..03394d4b3b6 100644
--- a/ui/assets/styles/dark-theme.css
+++ b/ui/assets/styles/dark-theme.css
@@ -2560,6 +2560,9 @@ z-select,
   z-select:focus,
   .z-select:focus {
     outline: none; }
+  z-select ul > li.default-option,
+  .z-select ul > li.default-option {
+    font-style: italic; }
   z-select button.focusable,
   .z-select button.focusable {
     position: relative;
diff --git a/ui/assets/styles/hc-dark.css b/ui/assets/styles/hc-dark.css
index 1a0e41eb4e4..70c550341ba 100644
--- a/ui/assets/styles/hc-dark.css
+++ b/ui/assets/styles/hc-dark.css
@@ -2536,6 +2536,9 @@ z-select,
   z-select:focus,
   .z-select:focus {
     outline: none; }
+  z-select ul > li.default-option,
+  .z-select ul > li.default-option {
+    font-style: italic; }
   z-select button.focusable,
   .z-select button.focusable {
     position: relative;
diff --git a/ui/assets/styles/hc-light.css b/ui/assets/styles/hc-light.css
index 1403a01f88f..1509b5603ce 100644
--- a/ui/assets/styles/hc-light.css
+++ b/ui/assets/styles/hc-light.css
@@ -2536,6 +2536,9 @@ z-select,
   z-select:focus,
   .z-select:focus {
     outline: none; }
+  z-select ul > li.default-option,
+  .z-select ul > li.default-option {
+    font-style: italic; }
   z-select button.focusable,
   .z-select button.focusable {
     position: relative;
diff --git a/ui/include/classes/api/API.php b/ui/include/classes/api/API.php
index 6813e152f88..23bec67d17a 100644
--- a/ui/include/classes/api/API.php
+++ b/ui/include/classes/api/API.php
@@ -444,6 +444,13 @@ class API {
 		return self::getApi('user');
 	}
 
+	/**
+	 * @return CUserDirectory
+	 */
+	public static function UserDirectory() {
+		return self::getApi('userdirectory');
+	}
+
 	/**
 	 * @return CUserGroup
 	 */
diff --git a/ui/include/classes/api/CApiServiceFactory.php b/ui/include/classes/api/CApiServiceFactory.php
index 00d8c34263f..31286652cc9 100644
--- a/ui/include/classes/api/CApiServiceFactory.php
+++ b/ui/include/classes/api/CApiServiceFactory.php
@@ -76,6 +76,7 @@ class CApiServiceFactory extends CRegistryFactory {
 		'trigger' => CTrigger::class,
 		'triggerprototype' => CTriggerPrototype::class,
 		'user' => CUser::class,
+		'userdirectory' => CUserDirectory::class,
 		'usergroup' => CUserGroup::class,
 		'usermacro' => CUserMacro::class,
 		'valuemap' => CValueMap::class
diff --git a/ui/include/classes/api/CAudit.php b/ui/include/classes/api/CAudit.php
index e79ac8581ac..f1d7f3b500f 100644
--- a/ui/include/classes/api/CAudit.php
+++ b/ui/include/classes/api/CAudit.php
@@ -82,6 +82,7 @@ class CAudit {
 	public const RESOURCE_SCHEDULED_REPORT = 46;
 	public const RESOURCE_HA_NODE = 47;
 	public const RESOURCE_SLA = 48;
+	public const RESOURCE_USERDIRECTORY = 49;
 
 	/**
 	 * Audit details actions.
@@ -133,6 +134,7 @@ class CAudit {
 		self::RESOURCE_TEMPLATE => 'hosts',
 		self::RESOURCE_TEMPLATE_DASHBOARD => 'dashboard',
 		self::RESOURCE_USER => 'users',
+		self::RESOURCE_USERDIRECTORY => 'userdirectory',
 		self::RESOURCE_USER_GROUP => 'usrgrp'
 	];
 
@@ -181,6 +183,7 @@ class CAudit {
 		self::RESOURCE_TEMPLATE => 'host',
 		self::RESOURCE_TEMPLATE_DASHBOARD => 'name',
 		self::RESOURCE_USER => 'username',
+		self::RESOURCE_USERDIRECTORY => 'name',
 		self::RESOURCE_USER_GROUP => 'name'
 	];
 
@@ -218,6 +221,7 @@ class CAudit {
 		self::RESOURCE_TEMPLATE => 'template',
 		self::RESOURCE_TEMPLATE_DASHBOARD => 'templatedashboard',
 		self::RESOURCE_USER => 'user',
+		self::RESOURCE_USERDIRECTORY => 'userdirectory',
 		self::RESOURCE_USER_GROUP => 'usergroup'
 	];
 
@@ -247,7 +251,8 @@ class CAudit {
 			'paths' => ['template.macros.value'],
 			'conditions' => ['type' => ZBX_MACRO_TYPE_SECRET]
 		],
-		self::RESOURCE_USER => ['paths' => ['user.passwd']]
+		self::RESOURCE_USER => ['paths' => ['user.passwd']],
+		self::RESOURCE_USERDIRECTORY => ['paths' => ['userdirectory.bind_dn', 'userdirectory.bind_password']]
 	];
 
 	/**
diff --git a/ui/include/classes/api/services/CAuditLog.php b/ui/include/classes/api/services/CAuditLog.php
index e809b05eb18..ee0085982fb 100644
--- a/ui/include/classes/api/services/CAuditLog.php
+++ b/ui/include/classes/api/services/CAuditLog.php
@@ -92,7 +92,8 @@ class CAuditLog extends CApiService {
 			CAudit::RESOURCE_SCHEDULED_REPORT, CAudit::RESOURCE_SCRIPT, CAudit::RESOURCE_SETTINGS, CAudit::RESOURCE_SLA,
 			CAudit::RESOURCE_TEMPLATE, CAudit::RESOURCE_TEMPLATE_DASHBOARD, CAudit::RESOURCE_TRIGGER,
 			CAudit::RESOURCE_TRIGGER_PROTOTYPE, CAudit::RESOURCE_USER, CAudit::RESOURCE_USER_GROUP,
-			CAudit::RESOURCE_USER_ROLE, CAudit::RESOURCE_VALUE_MAP, CAudit::RESOURCE_HA_NODE
+			CAudit::RESOURCE_USER_ROLE, CAudit::RESOURCE_VALUE_MAP, CAudit::RESOURCE_HA_NODE,
+			CAudit::RESOURCE_USERDIRECTORY
 		];
 
 		$api_input_rules = ['type' => API_OBJECT, 'fields' => [
diff --git a/ui/include/classes/api/services/CAuthentication.php b/ui/include/classes/api/services/CAuthentication.php
index ca1100d5de0..56cda2ce5ba 100644
--- a/ui/include/classes/api/services/CAuthentication.php
+++ b/ui/include/classes/api/services/CAuthentication.php
@@ -36,12 +36,11 @@ class CAuthentication extends CApiService {
 	 * @var array
 	 */
 	private $output_fields = ['authentication_type', 'http_auth_enabled', 'http_login_form', 'http_strip_domains',
-		'http_case_sensitive', 'ldap_configured', 'ldap_host', 'ldap_port', 'ldap_base_dn', 'ldap_search_attribute',
-		'ldap_bind_dn', 'ldap_case_sensitive', 'ldap_bind_password', 'saml_auth_enabled', 'saml_idp_entityid',
-		'saml_sso_url', 'saml_slo_url', 'saml_username_attribute', 'saml_sp_entityid', 'saml_nameid_format',
-		'saml_sign_messages', 'saml_sign_assertions', 'saml_sign_authn_requests', 'saml_sign_logout_requests',
-		'saml_sign_logout_responses', 'saml_encrypt_nameid', 'saml_encrypt_assertions', 'saml_case_sensitive',
-		'passwd_min_length', 'passwd_check_rules'
+		'http_case_sensitive', 'ldap_configured', 'ldap_case_sensitive', 'ldap_userdirectoryid', 'saml_auth_enabled',
+		'saml_idp_entityid', 'saml_sso_url', 'saml_slo_url', 'saml_username_attribute', 'saml_sp_entityid',
+		'saml_nameid_format', 'saml_sign_messages', 'saml_sign_assertions', 'saml_sign_authn_requests',
+		'saml_sign_logout_requests', 'saml_sign_logout_responses', 'saml_encrypt_nameid', 'saml_encrypt_assertions',
+		'saml_case_sensitive', 'passwd_min_length', 'passwd_check_rules'
 	];
 
 	/**
@@ -122,13 +121,8 @@ class CAuthentication extends CApiService {
 			'http_strip_domains' =>			['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('config', 'http_strip_domains')],
 			'http_case_sensitive' =>		['type' => API_INT32, 'in' => ZBX_AUTH_CASE_INSENSITIVE.','.ZBX_AUTH_CASE_SENSITIVE],
 			'ldap_configured' =>			['type' => API_INT32, 'in' => ZBX_AUTH_LDAP_DISABLED.','.ZBX_AUTH_LDAP_ENABLED],
-			'ldap_host' =>					['type' => API_STRING_UTF8, 'flags' => API_NOT_EMPTY, 'length' => DB::getFieldLength('config', 'ldap_host')],
-			'ldap_port' =>					['type' => API_INT32, 'in' => '0:65535'],
-			'ldap_base_dn' =>				['type' => API_STRING_UTF8, 'flags' => API_NOT_EMPTY, 'length' => DB::getFieldLength('config', 'ldap_base_dn')],
-			'ldap_search_attribute' =>		['type' => API_STRING_UTF8, 'flags' => API_NOT_EMPTY, 'length' => DB::getFieldLength('config', 'ldap_search_attribute')],
-			'ldap_bind_dn' =>				['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('config', 'ldap_bind_dn')],
 			'ldap_case_sensitive' =>		['type' => API_INT32, 'in' => ZBX_AUTH_CASE_INSENSITIVE.','.ZBX_AUTH_CASE_SENSITIVE],
-			'ldap_bind_password' =>			['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('config', 'ldap_bind_password')],
+			'ldap_userdirectoryid' =>		['type' => API_ID, 'flags' => API_ALLOW_NULL],
 			'saml_auth_enabled' =>			['type' => API_INT32, 'in' => ZBX_AUTH_SAML_DISABLED.','.ZBX_AUTH_SAML_ENABLED],
 			'saml_idp_entityid' =>			['type' => API_STRING_UTF8, 'flags' => API_NOT_EMPTY, 'length' => DB::getFieldLength('config', 'saml_idp_entityid')],
 			'saml_sso_url' =>				['type' => API_STRING_UTF8, 'flags' => API_NOT_EMPTY, 'length' => DB::getFieldLength('config', 'saml_sso_url')],
@@ -152,9 +146,42 @@ class CAuthentication extends CApiService {
 			self::exception(ZBX_API_ERROR_PARAMETERS, $error);
 		}
 
+		if (array_key_exists('ldap_userdirectoryid', $auth) && $auth['ldap_userdirectoryid']) {
+			$exists = API::UserDirectory()->get(['userdirectoryids' => $auth['ldap_userdirectoryid']]);
+
+			if (!$exists) {
+				static::exception(ZBX_API_ERROR_PARAMETERS,
+					_s('Invalid parameter "%1$s": %2$s.', '/ldap_userdirectoryid', _('referred object does not exist'))
+				);
+			}
+		}
+
 		$output_fields = $this->output_fields;
 		$output_fields[] = 'configid';
 
-		return DB::select('config', ['output' => $output_fields])[0];
+		$db_auth = DB::select('config', ['output' => $output_fields]);
+		$db_auth = reset($db_auth);
+
+		if (array_key_exists('authentication_type', $auth)) {
+			$auth += $db_auth;
+
+			if ($auth['authentication_type'] == ZBX_AUTH_LDAP
+					&& $auth['ldap_configured'] == ZBX_AUTH_LDAP_DISABLED) {
+				static::exception(ZBX_API_ERROR_PARAMETERS,
+					_s('Incorrect value for field "%1$s": %2$s.', '/authentication_type', _('LDAP must be enabled'))
+				);
+			}
+
+			$have_userdirectories = $auth['ldap_userdirectoryid'] != 0
+				|| API::UserDirectory()->get(['output' => ['userdirectoryid'], 'limit' => 1]);
+
+			if (!$have_userdirectories) {
+				static::exception(ZBX_API_ERROR_PARAMETERS,
+					_s('Incorrect value for field "%1$s": %2$s.', '/authentication_type', _('no LDAP servers'))
+				);
+			}
+		}
+
+		return $db_auth;
 	}
 }
diff --git a/ui/include/classes/api/services/CUser.php b/ui/include/classes/api/services/CUser.php
index 808f604d69e..ed999cbbec3 100644
--- a/ui/include/classes/api/services/CUser.php
+++ b/ui/include/classes/api/services/CUser.php
@@ -1380,54 +1380,6 @@ class CUser extends CApiService {
 		}
 	}
 
-	/**
-	 * Authenticate a user using LDAP.
-	 *
-	 * The $user array must have the following attributes:
-	 * - username   - user name
-	 * - password   - user password
-	 *
-	 * @param array $user
-	 *
-	 * @return bool
-	 */
-	protected function ldapLogin(array $user) {
-		$cnf = [];
-		$auth_params = [
-			CAuthenticationHelper::LDAP_CASE_SENSITIVE,
-			CAuthenticationHelper::LDAP_CONFIGURED,
-			CAuthenticationHelper::LDAP_HOST,
-			CAuthenticationHelper::LDAP_PORT,
-			CAuthenticationHelper::LDAP_BASE_DN,
-			CAuthenticationHelper::LDAP_BIND_DN,
-			CAuthenticationHelper::LDAP_SEARCH_ATTRIBUTE,
-			CAuthenticationHelper::LDAP_BIND_PASSWORD
-		];
-
-		foreach ($auth_params as $param) {
-			$cnf[str_replace('ldap_', '', $param)] = CAuthenticationHelper::get($param);
-		}
-
-		$ldap_status = (new CFrontendSetup())->checkPhpLdapModule();
-
-		if ($ldap_status['result'] != CFrontendSetup::CHECK_OK) {
-			self::exception(ZBX_API_ERROR_PARAMETERS, $ldap_status['error']);
-		}
-
-		$ldapValidator = new CLdapAuthValidator(['conf' => $cnf]);
-
-		if ($ldapValidator->validate($user)) {
-			return true;
-		}
-		else {
-			self::exception($ldapValidator->isConnectionError()
-					? ZBX_API_ERROR_PARAMETERS
-					: ZBX_API_ERROR_PERMISSIONS,
-				$ldapValidator->getError()
-			);
-		}
-	}
-
 	public function logout($user) {
 		$api_input_rules = ['type' => API_OBJECT, 'fields' => []];
 		if (!CApiInputValidator::validate($api_input_rules, $user, '/', $error)) {
@@ -1535,7 +1487,34 @@ class CUser extends CApiService {
 		try {
 			switch ($group_to_auth_map[$db_user['gui_access']]) {
 				case ZBX_AUTH_LDAP:
-					$this->ldapLogin($user);
+					if (CAuthenticationHelper::get(CAuthenticationHelper::LDAP_CONFIGURED) == ZBX_AUTH_LDAP_DISABLED) {
+						self::exception(ZBX_API_ERROR_INTERNAL, _('LDAP authentication is disabled.'));
+					}
+
+					$id = $db_user['userdirectoryid'] != 0
+						? $db_user['userdirectoryid']
+						: CAuthenticationHelper::get(CAuthenticationHelper::LDAP_USERDIRECTORYID);
+					$userdirectory = [];
+
+					if ($id != 0) {
+						$userdirectory = API::UserDirectory()->get([
+							'output' => ['userdirectoryid', 'host', 'port', 'base_dn', 'bind_dn', 'search_attribute',
+								'search_filter', 'start_tls'
+							],
+							'userdirectoryids' => $id
+						]);
+						$userdirectory = reset($userdirectory);
+					}
+
+					if (!$userdirectory) {
+						self::exception(ZBX_API_ERROR_INTERNAL, _('Cannot find user directory for LDAP.'));
+					}
+
+					$userdirectory += [
+						'test_username' => $user['username'],
+						'test_password' => $user['password']
+					];
+					API::UserDirectory()->test($userdirectory);
 					break;
 
 				case ZBX_AUTH_INTERNAL:
@@ -1548,7 +1527,6 @@ class CUser extends CApiService {
 
 				default:
 					self::exception(ZBX_API_ERROR_PERMISSIONS, _('No permissions for system access.'));
-					break;
 			}
 		}
 		catch (APIException $e) {
@@ -1809,25 +1787,65 @@ class CUser extends CApiService {
 		$permissions = [
 			'debug_mode' => GROUP_DEBUG_MODE_DISABLED,
 			'users_status' => GROUP_STATUS_ENABLED,
-			'gui_access' => GROUP_GUI_ACCESS_SYSTEM
+			'gui_access' => GROUP_GUI_ACCESS_SYSTEM,
+			'userdirectoryid' => 0
 		];
 
 		$db_usrgrps = DBselect(
-			'SELECT g.debug_mode,g.users_status,g.gui_access'.
+			'SELECT g.debug_mode,g.users_status,g.gui_access,g.userdirectoryid'.
 			' FROM usrgrp g,users_groups ug'.
 			' WHERE g.usrgrpid=ug.usrgrpid'.
 				' AND ug.userid='.$userid
 		);
 
+		$userdirectoryids = [];
+
 		while ($db_usrgrp = DBfetch($db_usrgrps)) {
 			if ($db_usrgrp['debug_mode'] == GROUP_DEBUG_MODE_ENABLED) {
 				$permissions['debug_mode'] = GROUP_DEBUG_MODE_ENABLED;
 			}
+
 			if ($db_usrgrp['users_status'] == GROUP_STATUS_DISABLED) {
 				$permissions['users_status'] = GROUP_STATUS_DISABLED;
 			}
+
 			if ($db_usrgrp['gui_access'] > $permissions['gui_access']) {
 				$permissions['gui_access'] = $db_usrgrp['gui_access'];
+				$userdirectoryids = [];
+			}
+
+			if ($permissions['gui_access'] === $db_usrgrp['gui_access']
+					&& ($db_usrgrp['gui_access'] == GROUP_GUI_ACCESS_LDAP
+						|| ($db_usrgrp['gui_access'] == GROUP_GUI_ACCESS_SYSTEM
+							&& CAuthenticationHelper::get(CAuthenticationHelper::AUTHENTICATION_TYPE) == ZBX_AUTH_LDAP
+			))) {
+				$userdirectoryids[$db_usrgrp['userdirectoryid']] = true;
+			}
+		}
+
+		if ($userdirectoryids) {
+			if (array_key_exists(0, $userdirectoryids)) {
+				unset($userdirectoryids[0]);
+				$default_userdirectoryid = CAuthenticationHelper::get(CAuthenticationHelper::LDAP_USERDIRECTORYID);
+
+				if ($default_userdirectoryid != 0) {
+					$userdirectoryids[$default_userdirectoryid] = true;
+				}
+			}
+
+			if (count($userdirectoryids) === 1) {
+				$permissions['userdirectoryid'] = array_keys($userdirectoryids)[0];
+			}
+			elseif (count($userdirectoryids) > 1) {
+				$db_userdirectoryids = API::UserDirectory()->get([
+					'output' => [],
+					'userdirectoryids' => array_keys($userdirectoryids),
+					'sortfield' => ['name'],
+					'limit' => 1,
+					'preservekeys' => true
+				]);
+
+				$permissions['userdirectoryid'] = array_keys($db_userdirectoryids)[0];
 			}
 		}
 
@@ -2062,6 +2080,7 @@ class CUser extends CApiService {
 	 * @param array  $permissions
 	 * @param string $permissions['debug_mode']
 	 * @param string $permissions['gui_access']
+	 * @param string $permissions['userdirectoryid']
 	 */
 	private function addExtraFields(array $db_user, array $permissions): array {
 		$db_user['type'] = $this->getUserType($db_user['roleid']);
@@ -2069,6 +2088,7 @@ class CUser extends CApiService {
 
 		$db_user['debug_mode'] = $permissions['debug_mode'];
 		$db_user['gui_access'] = $permissions['gui_access'];
+		$db_user['userdirectoryid'] = $permissions['userdirectoryid'];
 
 		if ($db_user['lang'] === LANG_DEFAULT) {
 			$db_user['lang'] = CSettingsHelper::getGlobal(CSettingsHelper::DEFAULT_LANG);
diff --git a/ui/include/classes/api/services/CUserDirectory.php b/ui/include/classes/api/services/CUserDirectory.php
new file mode 100644
index 00000000000..71eddb21976
--- /dev/null
+++ b/ui/include/classes/api/services/CUserDirectory.php
@@ -0,0 +1,481 @@
+<?php declare(strict_types = 1);
+/*
+** Zabbix
+** Copyright (C) 2001-2022 Zabbix SIA
+**
+** This program is free software; you can redistribute it and/or modify
+** it under the terms of the GNU General Public License as published by
+** the Free Software Foundation; either version 2 of the License, or
+** (at your option) any later version.
+**
+** This program is distributed in the hope that it will be useful,
+** but WITHOUT ANY WARRANTY; without even the implied warranty of
+** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+** GNU General Public License for more details.
+**
+** You should have received a copy of the GNU General Public License
+** along with this program; if not, write to the Free Software
+** Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+**/
+
+
+class CUserDirectory extends CApiService {
+
+	public const ACCESS_RULES = [
+		'get' => ['min_user_type' => USER_TYPE_SUPER_ADMIN],
+		'create' => ['min_user_type' => USER_TYPE_SUPER_ADMIN],
+		'update' => ['min_user_type' => USER_TYPE_SUPER_ADMIN],
+		'delete' => ['min_user_type' => USER_TYPE_SUPER_ADMIN],
+		'test' => ['min_user_type' => USER_TYPE_SUPER_ADMIN]
+	];
+
+	protected $tableName = 'userdirectory';
+	protected $tableAlias = 'ud';
+	protected $sortColumns = ['host', 'name'];
+
+	/**
+	 * @var array
+	 */
+	protected $output_fields = ['userdirectoryid', 'name', 'description', 'host', 'port', 'base_dn', 'bind_dn',
+		'search_attribute', 'start_tls', 'search_filter'
+	];
+
+	/**
+	 * @param array $options
+	 *
+	 * @throws APIException
+	 *
+	 * @return array|string
+	 */
+	public function get(array $options) {
+		$api_input_rules = ['type' => API_OBJECT, 'fields' => [
+			// filter
+			'userdirectoryids' =>			['type' => API_IDS, 'flags' => API_ALLOW_NULL | API_NORMALIZE, 'default' => null],
+			'filter' =>						['type' => API_OBJECT, 'flags' => API_ALLOW_NULL, 'default' => null, 'fields' => [
+				'userdirectoryid' =>			['type' => API_IDS, 'flags' => API_ALLOW_NULL | API_NORMALIZE],
+				'host' =>						['type' => API_STRINGS_UTF8, 'flags' => API_ALLOW_NULL | API_NORMALIZE],
+				'name' =>						['type' => API_STRINGS_UTF8, 'flags' => API_ALLOW_NULL | API_NORMALIZE]
+			]],
+			'search' =>						['type' => API_OBJECT, 'flags' => API_ALLOW_NULL, 'default' => null, 'fields' => array_fill_keys(
+				['base_dn', 'bind_dn', 'description', 'host', 'name', 'search_attribute', 'search_filter'],
+				['type' => API_STRINGS_UTF8, 'flags' => API_ALLOW_NULL | API_NORMALIZE]
+			)],
+			'searchByAny' =>				['type' => API_BOOLEAN, 'default' => false],
+			'startSearch' =>				['type' => API_FLAG, 'default' => false],
+			'excludeSearch' =>				['type' => API_FLAG, 'default' => false],
+			'searchWildcardsEnabled' =>		['type' => API_BOOLEAN, 'default' => false],
+			// output
+			'output' =>						['type' => API_OUTPUT, 'in' => implode(',', $this->output_fields), 'default' => API_OUTPUT_EXTEND],
+			'countOutput' =>				['type' => API_FLAG, 'default' => false],
+			'selectUsrgrps' =>				['type' => API_OUTPUT, 'flags' => API_ALLOW_NULL | API_ALLOW_COUNT, 'in' => implode(',', ['usrgrpid', 'name', 'gui_access', 'users_status', 'debug_mode']), 'default' => null],
+			// sort and limit
+			'sortfield' =>					['type' => API_STRINGS_UTF8, 'flags' => API_NORMALIZE, 'in' => implode(',', $this->sortColumns), 'uniq' => true, 'default' => []],
+			'sortorder' =>					['type' => API_SORTORDER, 'default' => []],
+			'limit' =>						['type' => API_INT32, 'flags' => API_ALLOW_NULL, 'in' => '1:'.ZBX_MAX_INT32, 'default' => null],
+			// flags
+			'preservekeys' =>				['type' => API_BOOLEAN, 'default' => false],
+		]];
+
+		if (!CApiInputValidator::validate($api_input_rules, $options, '/', $error)) {
+			self::exception(ZBX_API_ERROR_PARAMETERS, $error);
+		}
+
+		if ($options['output'] === API_OUTPUT_EXTEND) {
+			$options['output'] = $this->output_fields;
+		}
+
+		$db_userdirectories = [];
+
+		$sql = $this->createSelectQuery($this->tableName, $options);
+		$resource = DBselect($sql, $options['limit']);
+
+		while ($row = DBfetch($resource)) {
+			if ($options['countOutput']) {
+				return $row['rowscount'];
+			}
+
+			$db_userdirectories[$row['userdirectoryid']] = $row;
+		}
+
+		if ($db_userdirectories) {
+			$db_userdirectories = $this->addRelatedObjects($options, $db_userdirectories);
+			$db_userdirectories = $this->unsetExtraFields($db_userdirectories, ['userdirectoryid'], $options['output']);
+
+			if (!$options['preservekeys']) {
+				$db_userdirectories = array_values($db_userdirectories);
+			}
+		}
+
+		return $db_userdirectories;
+	}
+
+	/**
+	 * @param array $options
+	 * @param array $result
+	 *
+	 * @return array
+	 */
+	protected function addRelatedObjects(array $options, array $result): array {
+		$result = parent::addRelatedObjects($options, $result);
+
+		self::addRelatedUserGroups($options, $result);
+
+		return $result;
+	}
+
+	/**
+	 * @param array $options
+	 * @param array $result
+	 */
+	private static function addRelatedUserGroups(array $options, array &$result): void {
+		if ($options['selectUsrgrps'] === null) {
+			return;
+		}
+
+		foreach ($result as &$row) {
+			$row['usrgrps'] = [];
+		}
+		unset($row);
+
+		if ($options['selectUsrgrps'] === API_OUTPUT_COUNT) {
+			$output = ['userdirectoryid'];
+		}
+		elseif ($options['selectUsrgrps'] === API_OUTPUT_EXTEND) {
+			$output = ['usrgrpid', 'name', 'gui_access', 'users_status', 'debug_mode', 'userdirectoryid'];
+		}
+		else {
+			$output = array_unique(array_merge(['userdirectoryid'], $options['selectUsrgrps']));
+		}
+
+		$db_usergroups = API::UserGroup()->get([
+			'output' => $output,
+			'filter' => ['userdirectoryid' => array_keys($result)]
+		]);
+
+		foreach ($db_usergroups as $db_usergroup) {
+			$result[$db_usergroup['userdirectoryid']]['usrgrps'][] =
+				array_diff_key($db_usergroup, array_flip(['userdirectoryid']));
+		}
+
+		if ($options['selectUsrgrps'] === API_OUTPUT_COUNT) {
+			foreach ($result as &$row) {
+				$row['usrgrps'] = (string) count($row['usrgrps']);
+			}
+			unset($row);
+		}
+	}
+
+	/**
+	 * @param array $userdirectories
+	 *
+	 * @throws APIException
+	 *
+	 * @return array
+	 */
+	public function create(array $userdirectories): array {
+		self::validateCreate($userdirectories);
+
+		$db_count = DB::select('userdirectory', ['countOutput' => true]);
+		$userdirectoryids = DB::insert('userdirectory', $userdirectories);
+
+		foreach ($userdirectories as $index => &$userdirectory) {
+			$userdirectory['userdirectoryid'] = $userdirectoryids[$index];
+		}
+		unset($userdirectory);
+
+		self::addAuditLog(CAudit::ACTION_ADD, CAudit::RESOURCE_USERDIRECTORY, $userdirectories);
+
+		if (!$db_count) {
+			API::Authentication()->update(['ldap_userdirectoryid' => reset($userdirectoryids)]);
+		}
+
+		return ['userdirectoryids' => $userdirectoryids];
+	}
+
+	/**
+	 * @param array $userdirectories
+	 *
+	 * @throws APIException
+	 */
+	private static function validateCreate(array &$userdirectories): void {
+		$api_input_rules = ['type' => API_OBJECTS, 'flags' => API_NOT_EMPTY | API_NORMALIZE, 'uniq' => [['name']], 'fields' => [
+			'name' =>				['type' => API_STRING_UTF8, 'flags' => API_REQUIRED | API_NOT_EMPTY, 'length' => DB::getFieldLength('userdirectory', 'name')],
+			'description' =>		['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('userdirectory', 'description')],
+			'host' =>				['type' => API_STRING_UTF8, 'flags' => API_REQUIRED | API_NOT_EMPTY, 'length' => DB::getFieldLength('userdirectory', 'host')],
+			'port' =>				['type' => API_PORT, 'flags' => API_REQUIRED | API_NOT_EMPTY],
+			'base_dn' =>			['type' => API_STRING_UTF8, 'flags' => API_REQUIRED | API_NOT_EMPTY, 'length' => DB::getFieldLength('userdirectory', 'base_dn')],
+			'bind_dn' =>			['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('userdirectory', 'bind_dn')],
+			'bind_password' =>		['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('userdirectory', 'bind_password')],
+			'search_attribute' =>	['type' => API_STRING_UTF8, 'flags' => API_REQUIRED | API_NOT_EMPTY, 'length' => DB::getFieldLength('userdirectory', 'search_attribute')],
+			'start_tls' =>			['type' => API_INT32, 'in' => ZBX_AUTH_START_TLS_OFF.','.ZBX_AUTH_START_TLS_ON],
+			'search_filter' =>		['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('userdirectory', 'search_filter')]
+		]];
+
+		if (!CApiInputValidator::validate($api_input_rules, $userdirectories, '/', $error)) {
+			self::exception(ZBX_API_ERROR_PARAMETERS, $error);
+		}
+
+		self::checkDuplicates($userdirectories);
+	}
+
+	/**
+	 * @param array $userdirectories
+	 *
+	 * @throws APIException
+	 *
+	 * @return array
+	 */
+	public function update(array $userdirectories): array {
+		self::validateUpdate($userdirectories, $db_userdirectories);
+
+		$upd_userdirectories = [];
+
+		foreach ($userdirectories as $userdirectory) {
+			$upd_userdirectory = DB::getUpdatedValues('userdirectory', $userdirectory,
+				$db_userdirectories[$userdirectory['userdirectoryid']]
+			);
+
+			if ($upd_userdirectory) {
+				$upd_userdirectories[] = [
+					'values' => $upd_userdirectory,
+					'where' => ['userdirectoryid' => $userdirectory['userdirectoryid']]
+				];
+			}
+		}
+
+		if ($upd_userdirectories) {
+			DB::update('userdirectory', $upd_userdirectories);
+
+			self::addAuditLog(CAudit::ACTION_UPDATE, CAudit::RESOURCE_USERDIRECTORY, $userdirectories,
+				$db_userdirectories
+			);
+		}
+
+		return ['userdirectoryids' => array_column($userdirectories, 'userdirectoryid')];
+	}
+
+	/**
+	 * @param array      $userdirectories
+	 * @param array|null $db_userdirectories
+	 *
+	 * @throws APIException
+	 */
+	private static function validateUpdate(array &$userdirectories, ?array &$db_userdirectories): void {
+		$api_input_rules = ['type' => API_OBJECTS, 'flags' => API_NOT_EMPTY | API_NORMALIZE, 'uniq' => [['userdirectoryid'], ['name']], 'fields' => [
+			'userdirectoryid' =>	['type' => API_ID, 'flags' => API_REQUIRED],
+			'name' =>				['type' => API_STRING_UTF8, 'flags' => API_NOT_EMPTY, 'length' => DB::getFieldLength('userdirectory', 'name')],
+			'description' =>		['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('userdirectory', 'description')],
+			'host' =>				['type' => API_STRING_UTF8, 'flags' => API_NOT_EMPTY, 'length' => DB::getFieldLength('userdirectory', 'host')],
+			'port' =>				['type' => API_PORT, 'flags' => API_NOT_EMPTY],
+			'base_dn' =>			['type' => API_STRING_UTF8, 'flags' => API_NOT_EMPTY, 'length' => DB::getFieldLength('userdirectory', 'base_dn')],
+			'bind_dn' =>			['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('userdirectory', 'bind_dn')],
+			'bind_password' =>		['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('userdirectory', 'bind_password')],
+			'search_attribute' =>	['type' => API_STRING_UTF8, 'flags' => API_NOT_EMPTY, 'length' => DB::getFieldLength('userdirectory', 'search_attribute')],
+			'start_tls' =>			['type' => API_INT32, 'in' => ZBX_AUTH_START_TLS_OFF.','.ZBX_AUTH_START_TLS_ON],
+			'search_filter' =>		['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('userdirectory', 'search_filter')]
+		]];
+
+		if (!CApiInputValidator::validate($api_input_rules, $userdirectories, '/', $error)) {
+			self::exception(ZBX_API_ERROR_PARAMETERS, $error);
+		}
+
+		$db_userdirectories = DB::select('userdirectory', [
+			'output' => ['userdirectoryid', 'name', 'description', 'host', 'port', 'base_dn', 'bind_dn',
+				'bind_password', 'search_attribute', 'start_tls', 'search_filter'
+			],
+			'userdirectoryids' => array_column($userdirectories, 'userdirectoryid'),
+			'preservekeys' => true
+		]);
+
+		if (count($db_userdirectories) != count($userdirectories)) {
+			self::exception(ZBX_API_ERROR_PERMISSIONS, _('No permissions to referred object or it does not exist!'));
+		}
+
+		self::checkDuplicates($userdirectories, $db_userdirectories);
+	}
+
+	/**
+	 * Check for unique names.
+	 *
+	 * @param array      $userdirectories
+	 * @param array|null $db_userdirectories
+	 *
+	 * @throws APIException if userdirectory name is not unique.
+	 */
+	private static function checkDuplicates(array $userdirectories, array $db_userdirectories = null): void {
+		$names = [];
+
+		foreach ($userdirectories as $userdirectory) {
+			if (!array_key_exists('name', $userdirectory)) {
+				continue;
+			}
+
+			if ($db_userdirectories === null
+					|| $userdirectory['name'] !== $db_userdirectories[$userdirectory['userdirectoryid']]['name']) {
+				$names[] = $userdirectory['name'];
+			}
+		}
+
+		if (!$names) {
+			return;
+		}
+
+		$duplicates = DB::select('userdirectory', [
+			'output' => ['name'],
+			'filter' => ['name' => $names],
+			'limit' => 1
+		]);
+
+		if ($duplicates) {
+			self::exception(ZBX_API_ERROR_PARAMETERS, _s('User directory "%1$s" already exists.',
+				$duplicates[0]['name'])
+			);
+		}
+	}
+
+	/**
+	 * @param array $userdirectoryids
+	 *
+	 * @throws APIException
+	 *
+	 * @return array
+	 */
+	public function delete(array $userdirectoryids): array {
+		self::validateDelete($userdirectoryids, $db_userdirectories);
+
+		DB::delete('userdirectory', ['userdirectoryid' => $userdirectoryids]);
+
+		self::addAuditLog(CAudit::ACTION_DELETE, CAudit::RESOURCE_USERDIRECTORY, $db_userdirectories);
+
+		return ['userdirectoryids' => $userdirectoryids];
+	}
+
+	/**
+	 * @param array      $userdirectoryids
+	 * @param array|null $db_userdirectories
+	 *
+	 * @throws APIException
+	 */
+	private static function validateDelete(array $userdirectoryids, ?array &$db_userdirectories): void {
+		$api_input_rules = ['type' => API_IDS, 'flags' => API_NOT_EMPTY, 'uniq' => true];
+
+		if (!CApiInputValidator::validate($api_input_rules, $userdirectoryids, '/', $error)) {
+			self::exception(ZBX_API_ERROR_PARAMETERS, $error);
+		}
+
+		$db_userdirectories = API::UserDirectory()->get([
+			'output' => ['userdirectoryid', 'name'],
+			'userdirectoryids' => $userdirectoryids,
+			'preservekeys' => true
+		]);
+
+		if (count($db_userdirectories) != count($userdirectoryids)) {
+			self::exception(ZBX_API_ERROR_PERMISSIONS, _('No permissions to referred object or it does not exist!'));
+		}
+
+		$userdirectories_left = API::UserDirectory()->get(['countOutput' => true]) - count($userdirectoryids);
+		$auth = API::Authentication()->get([
+			'output' => ['ldap_userdirectoryid', 'authentication_type', 'ldap_configured']
+		]);
+
+		if (in_array($auth['ldap_userdirectoryid'], $userdirectoryids)
+				&& ($auth['ldap_configured'] == ZBX_AUTH_LDAP_ENABLED || $userdirectories_left > 0)) {
+			self::exception(ZBX_API_ERROR_PARAMETERS, _('Cannot delete default user directory.'));
+		}
+
+		$db_groups = API::UserGroup()->get([
+			'output' => ['userdirectoryid'],
+			'filter' => [
+				'gui_access' => [GROUP_GUI_ACCESS_LDAP, GROUP_GUI_ACCESS_SYSTEM],
+				'userdirectoryid' => $userdirectoryids
+			],
+			'limit' => 1
+		]);
+
+		if ($db_groups) {
+			$db_group = reset($db_groups);
+
+			self::exception(ZBX_API_ERROR_PARAMETERS, _s('Cannot delete user directory "%1$s".',
+				$db_userdirectories[$db_group['userdirectoryid']]['name'])
+			);
+		}
+
+		if (in_array($auth['ldap_userdirectoryid'], $userdirectoryids)) {
+			// If last (default) is removed, reset default userdirectoryid to prevent from foreign key constraint.
+			API::Authentication()->update(['ldap_userdirectoryid' => 0]);
+		}
+	}
+
+	/**
+	 * Test user against specific userdirectory connection.
+	 *
+	 * @param array $userdirectory
+	 *
+	 * @throws APIException
+	 *
+	 * @return bool
+	 */
+	public function test(array $userdirectory): bool {
+		self::validateTest($userdirectory);
+
+		$user = [
+			'username' => $userdirectory['test_username'],
+			'password' => $userdirectory['test_password']
+		];
+		$ldap_validator = new CLdapAuthValidator(['conf' => $userdirectory]);
+
+		if (!$ldap_validator->validate($user)) {
+			self::exception(
+				$ldap_validator->isConnectionError() ? ZBX_API_ERROR_PARAMETERS : ZBX_API_ERROR_PERMISSIONS,
+				$ldap_validator->getError()
+			);
+		}
+
+		return true;
+	}
+
+	/**
+	 * Validate user directory and test user credentials to be used for testing.
+	 *
+	 * @param array $userdirectory
+	 *
+	 * @throws APIException
+	 */
+	protected static function validateTest(array &$userdirectory): void {
+		$api_input_rules = ['type' => API_OBJECT, 'flags' => API_NOT_EMPTY | API_NORMALIZE, 'fields' => [
+			'userdirectoryid' =>	['type' => API_ID, 'flags' => API_ALLOW_NULL, 'default' => null],
+			'host' =>				['type' => API_STRING_UTF8, 'flags' => API_REQUIRED | API_NOT_EMPTY, 'length' => DB::getFieldLength('userdirectory', 'host')],
+			'port' =>				['type' => API_PORT, 'flags' => API_REQUIRED | API_NOT_EMPTY],
+			'base_dn' =>			['type' => API_STRING_UTF8, 'flags' => API_REQUIRED | API_NOT_EMPTY, 'length' => DB::getFieldLength('userdirectory', 'base_dn')],
+			'bind_dn' =>			['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('userdirectory', 'bind_dn'), 'default' => ''],
+			'bind_password' =>		['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('userdirectory', 'bind_password')],
+			'search_attribute' =>	['type' => API_STRING_UTF8, 'flags' => API_REQUIRED | API_NOT_EMPTY, 'length' => DB::getFieldLength('userdirectory', 'search_attribute')],
+			'start_tls' =>			['type' => API_INT32, 'in' => ZBX_AUTH_START_TLS_OFF.','.ZBX_AUTH_START_TLS_ON, 'default' => ZBX_AUTH_START_TLS_OFF],
+			'search_filter' =>		['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('userdirectory', 'search_filter'), 'default' => ''],
+			'test_username' =>		['type' => API_STRING_UTF8, 'flags' => API_REQUIRED | API_NOT_EMPTY],
+			'test_password' =>		['type' => API_STRING_UTF8, 'flags' => API_REQUIRED | API_NOT_EMPTY]
+		]];
+
+		if (!CApiInputValidator::validate($api_input_rules, $userdirectory, '/', $error)) {
+			self::exception(ZBX_API_ERROR_PARAMETERS, $error);
+		}
+
+		if ($userdirectory['userdirectoryid'] !== null) {
+			$db_userdirectory = DB::select('userdirectory', [
+				'output' => ['host', 'port', 'base_dn', 'bind_dn', 'bind_password', 'search_attribute', 'start_tls',
+					'search_filter'
+				],
+				'userdirectoryids' => $userdirectory['userdirectoryid']
+			]);
+			$db_userdirectory = reset($db_userdirectory);
+
+			if (!$db_userdirectory) {
+				self::exception(ZBX_API_ERROR_PERMISSIONS,
+					_('No permissions to referred object or it does not exist!')
+				);
+			}
+
+			$userdirectory += $db_userdirectory;
+		}
+	}
+}
diff --git a/ui/include/classes/api/services/CUserGroup.php b/ui/include/classes/api/services/CUserGroup.php
index 9bb5979dc11..c0d72cfac85 100644
--- a/ui/include/classes/api/services/CUserGroup.php
+++ b/ui/include/classes/api/services/CUserGroup.php
@@ -208,22 +208,26 @@ class CUserGroup extends CApiService {
 	 */
 	private function validateCreate(array &$usrgrps) {
 		$api_input_rules = ['type' => API_OBJECTS, 'flags' => API_NOT_EMPTY | API_NORMALIZE, 'uniq' => [['name']], 'fields' => [
-			'name' =>			['type' => API_STRING_UTF8, 'flags' => API_REQUIRED | API_NOT_EMPTY, 'length' => DB::getFieldLength('usrgrp', 'name')],
-			'debug_mode' =>		['type' => API_INT32, 'in' => implode(',', [GROUP_DEBUG_MODE_DISABLED, GROUP_DEBUG_MODE_ENABLED])],
-			'gui_access' =>		['type' => API_INT32, 'in' => implode(',', [GROUP_GUI_ACCESS_SYSTEM, GROUP_GUI_ACCESS_INTERNAL, GROUP_GUI_ACCESS_LDAP, GROUP_GUI_ACCESS_DISABLED])],
-			'users_status' =>	['type' => API_INT32, 'in' => implode(',', [GROUP_STATUS_ENABLED, GROUP_STATUS_DISABLED])],
-			'rights' =>			['type' => API_OBJECTS, 'flags' => API_NORMALIZE, 'uniq' => [['id']], 'fields' => [
-				'id' =>				['type' => API_ID, 'flags' => API_REQUIRED],
-				'permission' =>		['type' => API_INT32, 'flags' => API_REQUIRED, 'in' => implode(',', [PERM_DENY, PERM_READ, PERM_READ_WRITE])]
+			'name' =>				['type' => API_STRING_UTF8, 'flags' => API_REQUIRED | API_NOT_EMPTY, 'length' => DB::getFieldLength('usrgrp', 'name')],
+			'debug_mode' =>			['type' => API_INT32, 'in' => implode(',', [GROUP_DEBUG_MODE_DISABLED, GROUP_DEBUG_MODE_ENABLED])],
+			'gui_access' =>			['type' => API_INT32, 'in' => implode(',', [GROUP_GUI_ACCESS_SYSTEM, GROUP_GUI_ACCESS_INTERNAL, GROUP_GUI_ACCESS_LDAP, GROUP_GUI_ACCESS_DISABLED]), 'default' => DB::getDefault('usrgrp', 'gui_access')],
+			'users_status' =>		['type' => API_INT32, 'in' => implode(',', [GROUP_STATUS_ENABLED, GROUP_STATUS_DISABLED])],
+			'userdirectoryid' =>	['type' => API_MULTIPLE, 'rules' => [
+										['if' => ['field' => 'gui_access', 'in' => implode(',', [GROUP_GUI_ACCESS_SYSTEM, GROUP_GUI_ACCESS_LDAP])], 'type' => API_ID],
+										['else' => true, 'type' => API_UNEXPECTED]
 			]],
-			'tag_filters' =>	['type' => API_OBJECTS, 'uniq' => [['groupid', 'tag', 'value']], 'fields' => [
-				'groupid' =>		['type' => API_ID, 'flags' => API_REQUIRED],
-				'tag' =>			['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('tag_filter', 'tag'), 'default' => DB::getDefault('tag_filter', 'tag')],
-				'value' =>			['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('tag_filter', 'value'), 'default' => DB::getDefault('tag_filter', 'value')]
+			'rights' =>				['type' => API_OBJECTS, 'flags' => API_NORMALIZE, 'uniq' => [['id']], 'fields' => [
+				'id' =>					['type' => API_ID, 'flags' => API_REQUIRED],
+				'permission' =>			['type' => API_INT32, 'flags' => API_REQUIRED, 'in' => implode(',', [PERM_DENY, PERM_READ, PERM_READ_WRITE])]
 			]],
-			'userids' =>		['type' => API_IDS, 'flags' => API_NORMALIZE | API_DEPRECATED, 'uniq' => true],
-			'users' =>			['type' => API_OBJECTS, 'flags' => API_NORMALIZE, 'uniq' => [['userid']], 'fields' => [
-				'userid' =>			['type' => API_ID, 'flags' => API_REQUIRED]
+			'tag_filters' =>		['type' => API_OBJECTS, 'uniq' => [['groupid', 'tag', 'value']], 'fields' => [
+				'groupid' =>			['type' => API_ID, 'flags' => API_REQUIRED],
+				'tag' =>				['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('tag_filter', 'tag'), 'default' => DB::getDefault('tag_filter', 'tag')],
+				'value' =>				['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('tag_filter', 'value'), 'default' => DB::getDefault('tag_filter', 'value')]
+			]],
+			'userids' =>			['type' => API_IDS, 'flags' => API_NORMALIZE | API_DEPRECATED, 'uniq' => true],
+			'users' =>				['type' => API_OBJECTS, 'flags' => API_NORMALIZE, 'uniq' => [['userid']], 'fields' => [
+				'userid' =>				['type' => API_ID, 'flags' => API_REQUIRED]
 			]]
 		]];
 		if (!CApiInputValidator::validate($api_input_rules, $usrgrps, '/', $error)) {
@@ -247,6 +251,7 @@ class CUserGroup extends CApiService {
 		$this->checkHimself($usrgrps, __FUNCTION__);
 		$this->checkHostGroups($usrgrps);
 		$this->checkTagFilters($usrgrps);
+		self::checkUserDirectories($usrgrps);
 	}
 
 	/**
@@ -275,47 +280,57 @@ class CUserGroup extends CApiService {
 	 * @throws APIException if the input is invalid.
 	 */
 	private function validateUpdate(array &$usrgrps, array &$db_usrgrps = null) {
-		$api_input_rules = ['type' => API_OBJECTS, 'flags' => API_NOT_EMPTY | API_NORMALIZE, 'uniq' => [['usrgrpid'], ['name']], 'fields' => [
-			'usrgrpid' =>		['type' => API_ID, 'flags' => API_REQUIRED],
-			'name' =>			['type' => API_STRING_UTF8, 'flags' => API_NOT_EMPTY, 'length' => DB::getFieldLength('usrgrp', 'name')],
-			'debug_mode' =>		['type' => API_INT32, 'in' => implode(',', [GROUP_DEBUG_MODE_DISABLED, GROUP_DEBUG_MODE_ENABLED])],
-			'gui_access' =>		['type' => API_INT32, 'in' => implode(',', [GROUP_GUI_ACCESS_SYSTEM, GROUP_GUI_ACCESS_INTERNAL, GROUP_GUI_ACCESS_LDAP, GROUP_GUI_ACCESS_DISABLED])],
-			'users_status' =>	['type' => API_INT32, 'in' => implode(',', [GROUP_STATUS_ENABLED, GROUP_STATUS_DISABLED])],
-			'rights' =>			['type' => API_OBJECTS, 'flags' => API_NORMALIZE, 'uniq' => [['id']], 'fields' => [
-				'id' =>				['type' => API_ID, 'flags' => API_REQUIRED],
-				'permission' =>		['type' => API_INT32, 'flags' => API_REQUIRED, 'in' => implode(',', [PERM_DENY, PERM_READ, PERM_READ_WRITE])]
-			]],
-			'tag_filters' =>	['type' => API_OBJECTS, 'uniq' => [['groupid', 'tag', 'value']], 'fields' => [
-				'groupid' =>		['type' => API_ID, 'flags' => API_REQUIRED],
-				'tag' =>			['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('tag_filter', 'tag'), 'default' => DB::getDefault('tag_filter', 'tag')],
-				'value' =>			['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('tag_filter', 'value'), 'default' => DB::getDefault('tag_filter', 'value')]
-			]],
-			'userids' =>		['type' => API_IDS, 'flags' => API_NORMALIZE | API_DEPRECATED, 'uniq' => true],
-			'users' =>			['type' => API_OBJECTS, 'flags' => API_NORMALIZE, 'uniq' => [['userid']], 'fields' => [
-				'userid' =>			['type' => API_ID, 'flags' => API_REQUIRED]
-			]]
+		$api_input_rules = ['type' => API_OBJECTS, 'flags' => API_NOT_EMPTY | API_NORMALIZE | API_ALLOW_UNEXPECTED, 'uniq' => [['usrgrpid']], 'fields' => [
+			'usrgrpid' =>	['type' => API_ID, 'flags' => API_REQUIRED]
 		]];
+
 		if (!CApiInputValidator::validate($api_input_rules, $usrgrps, '/', $error)) {
 			self::exception(ZBX_API_ERROR_PARAMETERS, $error);
 		}
 
-		// Check user group names.
-		$db_usrgrps = DB::select('usrgrp', [
+		$usrgrpids = array_column($usrgrps, 'usrgrpid');
+		$db_usrgrps = API::UserGroup()->get([
 			'output' => ['usrgrpid', 'name', 'debug_mode', 'gui_access', 'users_status'],
-			'usrgrpids' => array_column($usrgrps, 'usrgrpid'),
+			'usrgrpids' => $usrgrpids,
 			'preservekeys' => true
 		]);
 
+		if (count($usrgrpids) != count($db_usrgrps)) {
+			self::exception(ZBX_API_ERROR_PERMISSIONS, _('No permissions to referred object or it does not exist!'));
+		}
+
 		$names = [];
+		$usrgrps = $this->extendObjectsByKey($usrgrps, $db_usrgrps, 'usrgrpid', ['gui_access']);
+		$api_input_rules = ['type' => API_OBJECTS, 'flags' => API_NOT_EMPTY | API_NORMALIZE, 'uniq' => [['name']], 'fields' => [
+			'usrgrpid' =>			['type' => API_ID],
+			'name' =>				['type' => API_STRING_UTF8, 'flags' => API_NOT_EMPTY, 'length' => DB::getFieldLength('usrgrp', 'name')],
+			'debug_mode' =>			['type' => API_INT32, 'in' => implode(',', [GROUP_DEBUG_MODE_DISABLED, GROUP_DEBUG_MODE_ENABLED])],
+			'gui_access' =>			['type' => API_INT32, 'in' => implode(',', [GROUP_GUI_ACCESS_SYSTEM, GROUP_GUI_ACCESS_INTERNAL, GROUP_GUI_ACCESS_LDAP, GROUP_GUI_ACCESS_DISABLED])],
+			'users_status' =>		['type' => API_INT32, 'in' => implode(',', [GROUP_STATUS_ENABLED, GROUP_STATUS_DISABLED])],
+			'userdirectoryid' =>	['type' => API_MULTIPLE, 'rules' => [
+										['if' => ['field' => 'gui_access', 'in' => implode(',', [GROUP_GUI_ACCESS_SYSTEM, GROUP_GUI_ACCESS_LDAP])], 'type' => API_ID],
+										['else' => true, 'type' => API_UNEXPECTED]
+			]],
+			'rights' =>				['type' => API_OBJECTS, 'flags' => API_NORMALIZE, 'uniq' => [['id']], 'fields' => [
+				'id' =>					['type' => API_ID, 'flags' => API_REQUIRED],
+				'permission' =>			['type' => API_INT32, 'flags' => API_REQUIRED, 'in' => implode(',', [PERM_DENY, PERM_READ, PERM_READ_WRITE])]
+			]],
+			'tag_filters' =>		['type' => API_OBJECTS, 'uniq' => [['groupid', 'tag', 'value']], 'fields' => [
+				'groupid' =>			['type' => API_ID, 'flags' => API_REQUIRED],
+				'tag' =>				['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('tag_filter', 'tag'), 'default' => DB::getDefault('tag_filter', 'tag')],
+				'value' =>				['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('tag_filter', 'value'), 'default' => DB::getDefault('tag_filter', 'value')]
+			]],
+			'userids' =>			['type' => API_IDS, 'flags' => API_NORMALIZE | API_DEPRECATED, 'uniq' => true],
+			'users' =>				['type' => API_OBJECTS, 'flags' => API_NORMALIZE, 'uniq' => [['userid']], 'fields' => [
+				'userid' =>				['type' => API_ID, 'flags' => API_REQUIRED]
+			]]
+		]];
 
-		foreach ($usrgrps as &$usrgrp) {
-			// Check if this user group exists.
-			if (!array_key_exists($usrgrp['usrgrpid'], $db_usrgrps)) {
-				self::exception(ZBX_API_ERROR_PERMISSIONS,
-					_('No permissions to referred object or it does not exist!')
-				);
-			}
+		if (!CApiInputValidator::validate($api_input_rules, $usrgrps, '/', $error)) {
+			self::exception(ZBX_API_ERROR_PARAMETERS, $error);
+		}
 
+		foreach ($usrgrps as &$usrgrp) {
 			if (array_key_exists('userids', $usrgrp)) {
 				if (array_key_exists('users', $usrgrp)) {
 					self::exception(ZBX_API_ERROR_PARAMETERS, _s('Parameter "%1$s" is deprecated.', 'userids'));
@@ -330,6 +345,12 @@ class CUserGroup extends CApiService {
 			if (array_key_exists('name', $usrgrp) && $usrgrp['name'] !== $db_usrgrp['name']) {
 				$names[] = $usrgrp['name'];
 			}
+
+			if (array_key_exists('gui_access', $usrgrp) && $usrgrp['gui_access'] != $db_usrgrp['gui_access']
+					&& $usrgrp['gui_access'] != GROUP_GUI_ACCESS_LDAP
+					&& $usrgrp['gui_access'] != GROUP_GUI_ACCESS_SYSTEM) {
+				$usrgrp['userdirectoryid'] = 0;
+			}
 		}
 		unset($usrgrp);
 
@@ -343,6 +364,7 @@ class CUserGroup extends CApiService {
 		$this->checkUsersWithoutGroups($usrgrps);
 		$this->checkHostGroups($usrgrps);
 		$this->checkTagFilters($usrgrps);
+		self::checkUserDirectories($usrgrps);
 	}
 
 	/**
@@ -1156,4 +1178,37 @@ class CUserGroup extends CApiService {
 			}
 		}
 	}
+
+	/**
+	 * Check if user directories exist.
+	 *
+	 * @param array  $usrgrps
+	 * @param string $usrgrps['userdirectoryid']
+	 *
+	 * @throws APIException
+	 */
+	private static function checkUserDirectories(array $usrgrps): void {
+		$userdirectoryids = array_filter(array_column($usrgrps, 'userdirectoryid'));
+
+		if (!$userdirectoryids) {
+			return;
+		}
+
+		$db_userdirectories = API::UserDirectory()->get([
+			'output' => [],
+			'userdirectoryids' => $userdirectoryids,
+			'preservekeys' => true
+		]);
+
+		foreach ($usrgrps as $i => $usrgrp) {
+			if (array_key_exists('userdirectoryid', $usrgrp) && $usrgrp['userdirectoryid'] != 0
+					&& !array_key_exists($usrgrp['userdirectoryid'], $db_userdirectories)) {
+				self::exception(ZBX_API_ERROR_PARAMETERS,
+					_s('Invalid parameter "%1$s": %2$s.', '/'.($i + 1).'/userdirectoryid',
+						_('referred object does not exist')
+					)
+				);
+			}
+		}
+	}
 }
diff --git a/ui/include/classes/helpers/CAuthenticationHelper.php b/ui/include/classes/helpers/CAuthenticationHelper.php
index 486231bb136..7a348532599 100644
--- a/ui/include/classes/helpers/CAuthenticationHelper.php
+++ b/ui/include/classes/helpers/CAuthenticationHelper.php
@@ -29,14 +29,9 @@ class CAuthenticationHelper extends CConfigGeneralHelper {
 	public const HTTP_CASE_SENSITIVE = 'http_case_sensitive';
 	public const HTTP_LOGIN_FORM = 'http_login_form';
 	public const HTTP_STRIP_DOMAINS = 'http_strip_domains';
-	public const LDAP_BASE_DN = 'ldap_base_dn';
-	public const LDAP_BIND_DN = 'ldap_bind_dn';
-	public const LDAP_BIND_PASSWORD = 'ldap_bind_password';
-	public const LDAP_CASE_SENSITIVE = 'ldap_case_sensitive';
 	public const LDAP_CONFIGURED = 'ldap_configured';
-	public const LDAP_HOST = 'ldap_host';
-	public const LDAP_PORT = 'ldap_port';
-	public const LDAP_SEARCH_ATTRIBUTE = 'ldap_search_attribute';
+	public const LDAP_USERDIRECTORYID = 'ldap_userdirectoryid';
+	public const LDAP_CASE_SENSITIVE = 'ldap_case_sensitive';
 	public const PASSWD_CHECK_RULES = 'passwd_check_rules';
 	public const PASSWD_MIN_LENGTH = 'passwd_min_length';
 	public const SAML_AUTH_ENABLED = 'saml_auth_enabled';
diff --git a/ui/include/classes/ldap/CLdap.php b/ui/include/classes/ldap/CLdap.php
index 5161ada9b9f..fb3be4e0865 100644
--- a/ui/include/classes/ldap/CLdap.php
+++ b/ui/include/classes/ldap/CLdap.php
@@ -31,6 +31,29 @@ class CLdap {
 	const ERR_OPT_REFERRALS_FAILED = 12;
 	const ERR_OPT_DEREF_FAILED = 13;
 
+	/**
+	 * @var array $cnf  LDAP connection settings.
+	 */
+	private $cnf = [
+		'host'				=> '',
+		'port'				=> '',
+		'bind_dn'			=> '',
+		'bind_password'		=> '',
+		'base_dn'			=> '',
+		'search_attribute'	=> '',
+		'search_filter'		=> '',
+		'groupkey'			=> 'cn',
+		'mapping'			=> [
+			'username' => 'uid',
+			'userid' => 'uidnumbera',
+			'passwd' => 'userpassword'
+		],
+		'referrals'			=> 0,
+		'version'			=> 3,
+		'start_tls'			=> ZBX_AUTH_START_TLS_OFF,
+		'deref' => null
+	];
+
 	/**
 	 * @var int
 	 */
@@ -39,30 +62,15 @@ class CLdap {
 	public function __construct($arg = []) {
 		$this->ds = false;
 		$this->info = [];
-		$this->cnf = [
-			'host' => 'ldap://localhost',
-			'port' => '389',
-			'bind_dn' => 'uid=admin,ou=system',
-			'bind_password' => '',
-			'base_dn' => 'ou=users,ou=system',
-			'search_attribute' => 'uid',
-			'userfilter' => '(%{attr}=%{user})',
-			'groupkey' => 'cn',
-			'mapping' => [
-				'username' => 'uid',
-				'userid' => 'uidnumbera',
-				'passwd' => 'userpassword'
-			],
-			'referrals' => 0,
-			'version' => 3,
-			'starttls' => null,
-			'deref' => null
-		];
 
 		if (is_array($arg)) {
 			$this->cnf = zbx_array_merge($this->cnf, $arg);
 		}
 
+		if ($this->cnf['search_filter'] === '') {
+			$this->cnf['search_filter'] = '(%{attr}=%{user})';
+		}
+
 		$this->error = $this->moduleEnabled() ? 0 : static::ERR_PHP_EXTENSION;
 	}
 
@@ -100,7 +108,7 @@ class CLdap {
 			}
 			else {
 				// use TLS (needs version 3)
-				if (isset($this->cnf['starttls']) && !@ldap_start_tls($this->ds)) {
+				if ($this->cnf['start_tls'] && !@ldap_start_tls($this->ds)) {
 					$this->error = static::ERR_OPT_TLS_FAILED;
 				}
 
@@ -144,7 +152,7 @@ class CLdap {
 
 			$this->bound = 2;
 		}
-		elseif (!empty($this->cnf['bind_dn']) && !empty($this->cnf['base_dn']) && !empty($this->cnf['userfilter'])) {
+		elseif (!empty($this->cnf['bind_dn']) && !empty($this->cnf['base_dn']) && !empty($this->cnf['search_filter'])) {
 			// special bind string
 			$dn = $this->makeFilter($this->cnf['bind_dn'], ['user' => $user, 'host' => $this->cnf['host']]);
 		}
@@ -217,13 +225,7 @@ class CLdap {
 
 		// get info for given user
 		$base = $this->makeFilter($this->cnf['base_dn'], $info);
-
-		if (isset($this->cnf['userfilter']) && !empty($this->cnf['userfilter'])) {
-			$filter = $this->makeFilter($this->cnf['userfilter'], $info);
-		}
-		else {
-			$filter = '(ObjectClass=*)';
-		}
+		$filter = $this->makeFilter($this->cnf['search_filter'], $info);
 		$sr = @ldap_search($this->ds, $base, $filter);
 		$result = is_resource($sr) ? @ldap_get_entries($this->ds, $sr) : [];
 
diff --git a/ui/include/classes/mvc/CRouter.php b/ui/include/classes/mvc/CRouter.php
index 591be2aa675..d46a09dbbef 100644
--- a/ui/include/classes/mvc/CRouter.php
+++ b/ui/include/classes/mvc/CRouter.php
@@ -171,6 +171,10 @@ class CRouter {
 		'popup.itemtest.edit'						=> ['CControllerPopupItemTestEdit',						'layout.json',			'popup.itemtestedit.view'],
 		'popup.itemtest.getvalue'					=> ['CControllerPopupItemTestGetValue',					'layout.json',			null],
 		'popup.itemtest.send'						=> ['CControllerPopupItemTestSend',						'layout.json',			null],
+		'popup.ldap.check'							=> ['CControllerPopupLdapCheck',						'layout.json',			null],
+		'popup.ldap.edit'							=> ['CControllerPopupLdapEdit',							'layout.json',			'popup.ldap.edit'],
+		'popup.ldap.test.edit'						=> ['CControllerPopupLdapTestEdit',						'layout.json',			'popup.ldap.test.edit'],
+		'popup.ldap.test.send'						=> ['CControllerPopupLdapTestSend',						'layout.json',			null],
 		'popup.lldoperation'						=> ['CControllerPopupLldOperation',						'layout.json',			'popup.lldoperation'],
 		'popup.lldoverride'							=> ['CControllerPopupLldOverride',						'layout.json',			'popup.lldoverride'],
 		'popup.maintenance.period'					=> ['CControllerPopupMaintenancePeriod',				'layout.json',			'popup.maintenance.period'],
diff --git a/ui/include/defines.inc.php b/ui/include/defines.inc.php
index f6c273f4772..7221924ae50 100644
--- a/ui/include/defines.inc.php
+++ b/ui/include/defines.inc.php
@@ -22,7 +22,7 @@ define('ZABBIX_VERSION',		'6.0.4rc1');
 define('ZABBIX_API_VERSION',	'6.0.4');
 define('ZABBIX_EXPORT_VERSION',	'6.0');
 
-define('ZABBIX_DB_VERSION',		6000000);
+define('ZABBIX_DB_VERSION',		6000015);
 
 define('DB_VERSION_SUPPORTED',				0);
 define('DB_VERSION_LOWER_THAN_MINIMUM',		1);
@@ -138,6 +138,8 @@ define('ZBX_AUTH_FORM_ZABBIX',	0);
 define('ZBX_AUTH_FORM_HTTP',	1);
 define('ZBX_AUTH_CASE_INSENSITIVE',	0);
 define('ZBX_AUTH_CASE_SENSITIVE',	1);
+define('ZBX_AUTH_START_TLS_OFF',	0);
+define('ZBX_AUTH_START_TLS_ON',		1);
 
 // password policy
 define('PASSWD_CHECK_CASE', 0x01);
@@ -2041,6 +2043,7 @@ define('ZBX_STYLE_ZABBIX_LOGO', 'zabbix-logo');
 define('ZBX_STYLE_ZABBIX_SIDEBAR_LOGO', 'zabbix-sidebar-logo');
 define('ZBX_STYLE_ZABBIX_SIDEBAR_LOGO_COMPACT', 'zabbix-sidebar-logo-compact');
 define('ZBX_STYLE_WIDGET_ITEM_LABEL', 'widget-item-label');
+define('ZBX_STYLE_DEFAULT_OPTION', 'default-option');
 
 // HTML column layout.
 define('ZBX_STYLE_COLUMNS', 'columns-wrapper');
diff --git a/ui/include/schema.inc.php b/ui/include/schema.inc.php
index 4730c6d22fc..b9f7bf35b47 100644
--- a/ui/include/schema.inc.php
+++ b/ui/include/schema.inc.php
@@ -1555,6 +1555,14 @@ return [
 				'type' => DB::FIELD_TYPE_INT,
 				'length' => 10,
 				'default' => '0'
+			],
+			'userdirectoryid' => [
+				'null' => true,
+				'type' => DB::FIELD_TYPE_ID,
+				'length' => 20,
+				'default' => NULL,
+				'ref_table' => 'userdirectory',
+				'ref_field' => 'userdirectoryid'
 			]
 		]
 	],
@@ -2148,42 +2156,6 @@ return [
 				'length' => 10,
 				'default' => '0'
 			],
-			'ldap_host' => [
-				'null' => false,
-				'type' => DB::FIELD_TYPE_CHAR,
-				'length' => 255,
-				'default' => ''
-			],
-			'ldap_port' => [
-				'null' => false,
-				'type' => DB::FIELD_TYPE_INT,
-				'length' => 10,
-				'default' => 389
-			],
-			'ldap_base_dn' => [
-				'null' => false,
-				'type' => DB::FIELD_TYPE_CHAR,
-				'length' => 255,
-				'default' => ''
-			],
-			'ldap_bind_dn' => [
-				'null' => false,
-				'type' => DB::FIELD_TYPE_CHAR,
-				'length' => 255,
-				'default' => ''
-			],
-			'ldap_bind_password' => [
-				'null' => false,
-				'type' => DB::FIELD_TYPE_CHAR,
-				'length' => 128,
-				'default' => ''
-			],
-			'ldap_search_attribute' => [
-				'null' => false,
-				'type' => DB::FIELD_TYPE_CHAR,
-				'length' => 128,
-				'default' => ''
-			],
 			'discovery_groupid' => [
 				'null' => false,
 				'type' => DB::FIELD_TYPE_ID,
@@ -2807,6 +2779,14 @@ return [
 				'type' => DB::FIELD_TYPE_CHAR,
 				'length' => 1024,
 				'default' => ''
+			],
+			'ldap_userdirectoryid' => [
+				'null' => true,
+				'type' => DB::FIELD_TYPE_ID,
+				'length' => 20,
+				'default' => NULL,
+				'ref_table' => 'userdirectory',
+				'ref_field' => 'userdirectoryid'
 			]
 		]
 	],
@@ -8894,6 +8874,75 @@ return [
 			]
 		]
 	],
+	'userdirectory' => [
+		'key' => 'userdirectoryid',
+		'fields' => [
+			'userdirectoryid' => [
+				'null' => false,
+				'type' => DB::FIELD_TYPE_ID,
+				'length' => 20
+			],
+			'name' => [
+				'null' => false,
+				'type' => DB::FIELD_TYPE_CHAR,
+				'length' => 128,
+				'default' => ''
+			],
+			'description' => [
+				'null' => false,
+				'type' => DB::FIELD_TYPE_TEXT,
+				'default' => ''
+			],
+			'host' => [
+				'null' => false,
+				'type' => DB::FIELD_TYPE_CHAR,
+				'length' => 255,
+				'default' => ''
+			],
+			'port' => [
+				'null' => false,
+				'type' => DB::FIELD_TYPE_INT,
+				'length' => 10,
+				'default' => 389
+			],
+			'base_dn' => [
+				'null' => false,
+				'type' => DB::FIELD_TYPE_CHAR,
+				'length' => 255,
+				'default' => ''
+			],
+			'bind_dn' => [
+				'null' => false,
+				'type' => DB::FIELD_TYPE_CHAR,
+				'length' => 255,
+				'default' => ''
+			],
+			'bind_password' => [
+				'null' => false,
+				'type' => DB::FIELD_TYPE_CHAR,
+				'length' => 128,
+				'default' => ''
+			],
+			'search_attribute' => [
+				'null' => false,
+				'type' => DB::FIELD_TYPE_CHAR,
+				'length' => 128,
+				'default' => ''
+			],
+			'start_tls' => [
+				'null' => false,
+				'type' => DB::FIELD_TYPE_INT,
+				'length' => 10,
+				'default' => '0'
+			],
+			'search_filter' => [
+				'null' => false,
+				'type' => DB::FIELD_TYPE_CHAR,
+				'length' => 255,
+				'default' => ''
+			]
+		]
+	],
 	'dbversion' => [
 		'key' => 'dbversionid',
 		'fields' => [
diff --git a/ui/js/class.cmap.js b/ui/js/class.cmap.js
index 1ea88b03721..6622744373d 100644
--- a/ui/js/class.cmap.js
+++ b/ui/js/class.cmap.js
@@ -91,6 +91,7 @@ ZABBIX.namespace('apps.map');
 ZABBIX.apps.map = (function($) {
 	// dependencies
 	var Observer = ZABBIX.classes.Observer;
+	const ZBX_STYLE_DEFAULT_OPTION = 'default-option';
 
 	function createMap(containerId, mapData) {
 		var CMap = function(containerId, mapData) {
@@ -2672,9 +2673,9 @@ ZABBIX.apps.map = (function($) {
 			const select_icon_maintenance = document.getElementById('iconid_maintenance');
 			const select_icon_disabled = document.getElementById('iconid_disabled');
 
-			select_icon_on.addOption({label: t('S_DEFAULT'), value: '0'});
-			select_icon_maintenance.addOption({label: t('S_DEFAULT'), value: '0'});
-			select_icon_disabled.addOption({label: t('S_DEFAULT'), value: '0'});
+			select_icon_on.addOption({label: t('S_DEFAULT'), value: '0', class_name: ZBX_STYLE_DEFAULT_OPTION});
+			select_icon_maintenance.addOption({label: t('S_DEFAULT'), value: '0', class_name: ZBX_STYLE_DEFAULT_OPTION});
+			select_icon_disabled.addOption({label: t('S_DEFAULT'), value: '0', class_name: ZBX_STYLE_DEFAULT_OPTION});
 
 			for (i in this.sysmap.iconList) {
 				icon = this.sysmap.iconList[i];
@@ -3252,9 +3253,9 @@ ZABBIX.apps.map = (function($) {
 			const select_icon_maintenance = document.getElementById('massIconidMaintenance');
 			const select_icon_disabled = document.getElementById('massIconidDisabled');
 
-			select_icon_on.addOption({label: t('S_DEFAULT'), value: '0'});
-			select_icon_maintenance.addOption({label: t('S_DEFAULT'), value: '0'});
-			select_icon_disabled.addOption({label: t('S_DEFAULT'), value: '0'});
+			select_icon_on.addOption({label: t('S_DEFAULT'), value: '0', class_name: ZBX_STYLE_DEFAULT_OPTION});
+			select_icon_maintenance.addOption({label: t('S_DEFAULT'), value: '0', class_name: ZBX_STYLE_DEFAULT_OPTION});
+			select_icon_disabled.addOption({label: t('S_DEFAULT'), value: '0', class_name: ZBX_STYLE_DEFAULT_OPTION});
 
 			for (i in this.sysmap.iconList) {
 				icon = this.sysmap.iconList[i];
