Цель состоит в том, чтобы позволить пользователям входить в систему с их учетными данными Active Directory.
Однако аутентификация завершается сбоем во время или сразу после фазы поиска с помощью этого ошибка:
LDAP retry search also failed for EmployeeID 1000- ResultCode: 1, Message: Operations Error,
ServerMessage: 000004DC: LdapErr: DSID-0C090CE5, comment: In order to perform this operation
a successful bind must be completed on the connection., data 0, v4563
Что обычно приводит к потере состояния привязки LDAP во время поиска и как можно предотвратить это в приложении .NET Blazor Server с помощью Novell.Directory.Ldap?
Должен ли я:
- Оставлять тот же экземпляр LdapConnection открытым для поиска и привязки?
- Избегать повторной привязки сервисного аккаунта между операциями?
- Использовать пул соединений или явно отключать рефералы?
Вот код:
// Add login endpoint
app.MapPost("/login-action", async (
[FromServices] SignInManager signInManager,
[FromServices] UserManager userManager,
[FromServices] LdapSettings ldapSettings,
HttpContext httpContext) =>
{
var form = await httpContext.Request.ReadFormAsync();
var employeeId = form["email"].ToString(); // Form field name is "email" but contains employee ID
var password = form["password"].ToString();
var authMethod = form["authMethod"].ToString();
var returnUrl = form["returnUrl"].ToString();
Log.Information("Authentication request received - Method: {AuthMethod}, EmployeeID: {EmployeeId}", authMethod, employeeId);
if (string.Equals(authMethod, "LDAP", StringComparison.OrdinalIgnoreCase))
{
// LDAP authentication logic
try
{
Log.Information("Attempting LDAP authentication for EmployeeID {EmployeeId}", employeeId);
// Validate input
if (string.IsNullOrWhiteSpace(employeeId) || employeeId.Length > 255)
{
Log.Warning("Invalid employee ID format: {EmployeeId}", employeeId);
return Results.Text("Invalid employee ID format.");
}
var sanitizedId = SanitizeLdapFilter(employeeId);
Log.Information("LDAP connecting to {Server}:{Port} (SSL: {UseSsl})",
ldapSettings.Server, ldapSettings.Port, ldapSettings.Port == 636);
using var connection = new LdapConnection();
// Configure SSL if using port 636 (LDAPS) or UseSsl is explicitly enabled
if (ldapSettings.Port == 636 || ldapSettings.UseSsl)
{
Log.Information("Configuring SSL/TLS for LDAPS connection");
connection.SecureSocketLayer = true;
// Configure certificate validation callback for SSL
if (ldapSettings.IgnoreSslCertificateErrors)
{
Log.Warning("LDAP SSL certificate validation will be bypassed (IgnoreSslCertificateErrors=true)");
connection.UserDefinedServerCertValidationDelegate += (object sender, X509Certificate? certificate,
X509Chain? chain, SslPolicyErrors sslPolicyErrors) =>
{
if (sslPolicyErrors != SslPolicyErrors.None)
{
Log.Warning("LDAP SSL certificate errors ignored: {PolicyErrors}", sslPolicyErrors);
}
return true; // Accept any certificate
};
}
}
connection.Connect(ldapSettings.Server, ldapSettings.Port);
// Disable referrals at connection level to prevent bind state loss
// This must be done after Connect but before Bind
connection.Constraints.ReferralFollowing = false;
Log.Information("LDAP referrals disabled at connection level");
Log.Information("LDAP connection established, binding with {BindDN}", ldapSettings.BindDN);
connection.Bind(ldapSettings.BindDN, ldapSettings.BindPassword);
Log.Information("LDAP bind successful");
// Create search constraints to prevent bind state loss
var searchConstraints = (LdapSearchConstraints)connection.SearchConstraints.Clone();
searchConstraints.ReferralFollowing = false; // Disable referrals to maintain bind state
searchConstraints.TimeLimit = ldapSettings.SearchTimeout; // Set timeout
searchConstraints.MaxResults = 100; // Reasonable limit
searchConstraints.Dereference = LdapSearchConstraints.DerefNever; // No dereferencing
Log.Information("LDAP search constraints configured - TimeLimit: {TimeLimit}ms, MaxResults: {MaxResults}, ReferralFollowing: {ReferralFollowing}",
ldapSettings.SearchTimeout, 100, false);
// Search for user
var searchFilter = $"(&(objectClass=user)(sAMAccountName={sanitizedId}))";
var attributes = new[] { "distinguishedName", "mail", "department", "title", "manager", "employeeID", "physicalDeliveryOfficeName" };
var searchBase = ldapSettings.SearchBase.Replace("DN:", "").Trim();
Log.Information("LDAP executing search - SearchBase: {SearchBase}, Filter: {Filter}", searchBase, searchFilter);
var searchResults = connection.Search(searchBase, LdapConnection.ScopeSub, searchFilter, attributes, false, searchConstraints);
Log.Information("LDAP search completed, immediately retrieving entry for EmployeeID {EmployeeId}", employeeId);
// Immediately re-bind after search to ensure connection state is fresh before result retrieval
// This is critical for LDAPS where bind state can expire during lazy result loading
Log.Information("Re-binding service account immediately after search for EmployeeID {EmployeeId}", employeeId);
connection.Bind(ldapSettings.BindDN, ldapSettings.BindPassword);
Log.Information("Service account re-bind successful immediately after search");
// Try to retrieve entry immediately - don't use HasMore() as it may use cached results
// while Next() requires actual server communication
LdapEntry userEntry;
try
{
userEntry = searchResults.Next();
Log.Information("LDAP user entry retrieved successfully - DN: {DN}", userEntry.Dn);
}
catch (LdapException ldapEx) when (ldapEx.ResultCode == LdapException.NoSuchObject || ldapEx.ResultCode == LdapException.SizeLimitExceeded)
{
// These errors indicate no results found (not bind issues)
Log.Warning("User not found: {EmployeeId} - ResultCode: {ResultCode}", employeeId, ldapEx.ResultCode);
return Results.Text("LDAP: User not found.");
}
catch (LdapException ldapEx) when (ldapEx.ResultCode == LdapException.OperationsError)
{
// Bind state was lost during result retrieval - reconnect, re-bind and retry search
Log.Warning("LDAP bind state lost during result retrieval for EmployeeID {EmployeeId} - reconnecting and retrying search", employeeId);
try
{
// Disconnect and reconnect to ensure fresh connection
if (connection.Connected)
{
connection.Disconnect();
}
}
catch (Exception disconnectEx)
{
Log.Warning("Error disconnecting LDAP connection: {Error}", disconnectEx.Message);
}
// Reconnect with SSL configuration
if (ldapSettings.Port == 636 || ldapSettings.UseSsl)
{
connection.SecureSocketLayer = true;
if (ldapSettings.IgnoreSslCertificateErrors)
{
connection.UserDefinedServerCertValidationDelegate += (object sender, X509Certificate? certificate,
X509Chain? chain, SslPolicyErrors sslPolicyErrors) => true;
}
}
connection.Connect(ldapSettings.Server, ldapSettings.Port);
// Disable referrals at connection level (must be after Connect but before Bind)
connection.Constraints.ReferralFollowing = false;
Log.Information("LDAP referrals disabled at connection level (retry)");
Log.Information("LDAP reconnection successful, re-binding service account");
connection.Bind(ldapSettings.BindDN, ldapSettings.BindPassword);
Log.Information("Service account re-bind successful, retrying search");
// Perform fresh search after reconnection and re-bind
searchResults = connection.Search(searchBase, LdapConnection.ScopeSub, searchFilter, attributes, false, searchConstraints);
// Re-bind again immediately after fresh search to ensure fresh connection state
Log.Information("Re-binding service account immediately after retry search for EmployeeID {EmployeeId}", employeeId);
connection.Bind(ldapSettings.BindDN, ldapSettings.BindPassword);
Log.Information("Service account re-bind successful after retry search");
try
{
// Try to retrieve entry immediately - don't use HasMore() as it may use cached results
Log.Information("Retrieving entry from retry search for EmployeeID {EmployeeId}", employeeId);
userEntry = searchResults.Next();
Log.Information("LDAP user entry retrieved successfully after retry - DN: {DN}", userEntry.Dn);
}
catch (LdapException retryEx) when (retryEx.ResultCode == LdapException.NoSuchObject || retryEx.ResultCode == LdapException.SizeLimitExceeded)
{
// These errors indicate no results found (not bind issues)
Log.Error("User not found after retry search for EmployeeID {EmployeeId} - ResultCode: {ResultCode}", employeeId, retryEx.ResultCode);
return Results.Text("LDAP: User not found.");
}
catch (LdapException retryEx) when (retryEx.ResultCode == LdapException.OperationsError)
{
Log.Error("LDAP retry search also failed for EmployeeID {EmployeeId} - ResultCode: {ResultCode}, Message: {Message}, ServerMessage: {ServerMessage}",
employeeId, retryEx.ResultCode, retryEx.Message, retryEx.LdapErrorMessage);
return Results.Text($"LDAP: Connection failed - {retryEx.Message}");
}
}
var userDn = userEntry.Dn;
Log.Information("LDAP user entry retrieved - DN: {DN}, proceeding with authentication", userDn);
// Extract attributes
var mail = GetAttributeValue(userEntry, "mail");
var department = GetAttributeValue(userEntry, "department");
var title = GetAttributeValue(userEntry, "title");
var manager = GetAttributeValue(userEntry, "manager");
var ldapEmployeeId = GetAttributeValue(userEntry, "employeeID");
var officeName = GetAttributeValue(userEntry, "physicalDeliveryOfficeName");
Log.Information("LDAP user found - DN: {DN}, Mail: {Mail}, Department: {Department}", userDn, mail, department);
// Validate department access
const string requiredDepartment = "Lojistik Yönetimi Direktörlüğü";
if (!string.Equals(officeName, requiredDepartment, StringComparison.OrdinalIgnoreCase))
{
Log.Warning("Access denied - Office: {OfficeName} (required: {RequiredDepartment})", officeName, requiredDepartment);
return Results.Text("Lojistik Yönetimi Direktörlüğüne bağlı çalışanlar sisteme giriş yapabilir. İlginiz için teşekkür ederiz.");
}
// Authenticate user with password
try
{
connection.Bind(userDn, password);
Log.Information("LDAP authentication successful for EmployeeID {EmployeeId}", employeeId);
}
catch
{
Log.Error("Invalid credentials for EmployeeID {EmployeeId}", employeeId);
return Results.Text("LDAP: Invalid username or password.");
}
// Create or find user in Identity
var user = await userManager.FindByNameAsync(employeeId);
if (user == null)
{
user = new IdentityUser { UserName = employeeId, Email = mail ?? employeeId, EmailConfirmed = true };
var createResult = await userManager.CreateAsync(user);
if (!createResult.Succeeded)
{
Log.Error("Failed to create user for EmployeeID {EmployeeId}", employeeId);
return Results.Text("LDAP: Failed to create user.");
}
await userManager.AddToRoleAsync(user, "Viewer");
Log.Information("Created new user for EmployeeID {EmployeeId}", employeeId);
}
await signInManager.SignInAsync(user, isPersistent: false);
return Results.Redirect(returnUrl ?? "/");
}
catch (LdapException ldapEx)
{
Log.Error("LDAP error for EmployeeID {EmployeeId} - ResultCode: {ResultCode}, Message: {Message}, ServerMessage: {ServerMessage}",
employeeId, ldapEx.ResultCode, ldapEx.Message, ldapEx.LdapErrorMessage);
return Results.Text($"LDAP: Connection failed - {ldapEx.Message}");
}
catch (Exception ex)
{
Log.Error("LDAP authentication error for EmployeeID {EmployeeId} - Type: {Type}, Message: {Message}, StackTrace: {StackTrace}",
employeeId, ex.GetType().Name, ex.Message, ex.StackTrace);
return Results.Text($"LDAP: Authentication failed - {ex.Message}");
}
}
else
{
// Local authentication (Identity)
Log.Information("Using Identity authentication for EmployeeID {EmployeeId}", employeeId);
var result = await signInManager.PasswordSignInAsync(employeeId, password, isPersistent: false, lockoutOnFailure: true);
if (result.Succeeded)
{
return Results.Redirect(returnUrl ?? "/");
}
string errorMessage;
if (result.IsLockedOut)
{
errorMessage = "Account is locked out. Please try again later.";
}
else
{
errorMessage = "Invalid email or password.";
}
return Results.Text(errorMessage);
}
});
Вот журналы:
2025-10-30 08:39:25.897 +03:00 [INF] Получен запрос аутентификации — метод: LDAP, идентификатор сотрудника: 1000
2025-10-30 08:39:25.899 +03:00 [INF] Попытка аутентификации LDAP для идентификатора сотрудника 1000
30 октября 2025 08:39:25.900 +03:00 [INF] Соединение LDAP с bbldaps.bb.intra:636 (SSL: true)
30 октября 2025 08:39:25.907 +03:00 [INF] Настройка SSL/TLS для подключения LDAPS
30 октября 2025 08:39:25.908 +03:00 [WRN] Проверка SSL-сертификата LDAP будет пропущена (IgnoreSslCertificateErrors=true)
30 октября 2025 г. 08:39:26.041 +03:00 [INF] Соединение LDAP установлено, привязка с CN=Logisticcisldap,OU=Users,OU=Service Accounts,DC=bb,DC=intra
2025-10-30 08:39:26.080 +03:00 [INF] Привязка LDAP выполнена успешно
2025-10-30 08:39:26.081 +03:00 [INF] Настроены ограничения поиска LDAP — TimeLimit: 5000 мс, MaxResults: 100, ReferralFollowing: false
2025-10-30 08:39:26.081 +03:00 [INF] LDAP выполняется search - SearchBase: OU=Personel,OU=bb,DC=bb,DC=intra, Filter: (&(objectClass=user)(sAMAccountName=1000))
2025-10-30 08:39:26.090 +03:00 [INF] Поиск по LDAP завершен, немедленно получена запись для сотрудника с идентификатором 1000
2025-10-30 08:39:26.090 +03:00 [INF] Повторная привязка сервисной учетной записи сразу после поиска по идентификатору сотрудника 1000
2025-10-30 08:39:26.110 +03:00 [INF] Повторная привязка сервисной учетной записи успешна сразу после поиска
2025-10-30 08:39:26.116 +03:00 [WRN] Потеряно состояние привязки LDAP во время получения результатов для идентификатора сотрудника 1000 — повторное подключение и повторная попытка поиска
2025-10-30 08:39:26.152 +03:00 [INF] Переподключение LDAP успешно, перепривязка службы учетная запись
2025-10-30 08:39:26.179 +03:00 [INF] Повторная привязка сервисной учетной записи успешна, повторная попытка поиска
2025-10-30 08:39:26.181 +03:00 [INF] Повторная привязка сервисной учетной записи сразу после повторной попытки поиска сотрудника с идентификатором 1000
2025-10-30 08:39:26.201 +03:00 [INF] Повторная привязка сервисной учетной записи успешна после повторной попытки поиска
2025-10-30 08:39:26.201 +03:00 [INF] Получение записи из повторного поиска для сотрудника с идентификатором 1000
2025-10-30 08:39:26.204 +03:00 [ERR] Повторная попытка поиска LDAP также не удалась для идентификатора сотрудника 1000. Код результата: 1, сообщение: ошибка операции, сообщение сервера: 000004DC: LdapErr: DSID-0C090CE5, комментарий: для выполнения этой операции необходимо завершить успешное связывание соединение., данные 0, v4563
Вот настройки приложения:
"LDAP": {
"Server": "bbldaps.bb.intra",
"Port": 636,
"UseSsl": true,
"IgnoreSslCertificateErrors": true,
"BindDN": "CN=logisticsldap,OU=Users,OU=Service Accounts,DC=bb,DC=intra",
"SearchBase": "OU=Personel,OU=bb,DC=bb,DC=intra",
"ConnectionTimeout": 10000,
"SearchTimeout": 5000,
"BindTimeout": 5000,
"MaxRetries": 3,
"RetryDelay": 1000
},
Подробнее здесь: https://stackoverflow.com/questions/798 ... -operation
Мобильная версия