I have a Windows Service that provides data through WCF to a Windows Forms application. The service takes care also of user authentication, validating user password using LDAP over the company Active Directory servers.
The problem is that it works for weeks (even months), than something happens and the LDAP user authentication fails with the following exception until I restart the service:
System.DirectoryServices.AccountManagement.PrincipalServerDownException: The server could not be contacted.
---> System.DirectoryServices.Protocols.LdapException: The LDAP server is unavailable.
at System.DirectoryServices.Protocols.LdapConnection.Connect()
at System.DirectoryServices.Protocols.LdapConnection.SendRequestHelper(DirectoryRequest request, Int32& messageID)
at System.DirectoryServices.Protocols.LdapConnection.SendRequest(DirectoryRequest request, TimeSpan requestTimeout)
at System.DirectoryServices.Protocols.LdapConnection.SendRequest(DirectoryRequest request)
at System.DirectoryServices.AccountManagement.PrincipalContext.ReadServerConfig(String serverName, ServerProperties& properties)
--- End of inner exception stack trace ---
at System.DirectoryServices.AccountManagement.PrincipalContext.ReadServerConfig(String serverName, ServerProperties& properties)
at System.DirectoryServices.AccountManagement.PrincipalContext.DoServerVerifyAndPropRetrieval()
at System.DirectoryServices.AccountManagement.PrincipalContext..ctor(ContextType contextType, String name, String container, ContextOptions options, String userName, String password)
at System.DirectoryServices.AccountManagement.PrincipalContext..ctor(ContextType contextType, String name, String userName, String password)
at SMSTModel.Authentication.ActiveDirectory.IsUserAllowed(String username, String password)
The service restart fixes the problem.
public static bool IsUserAllowed(string username, string password)
{
String localDomain = Domain.GetComputerDomain().Name;
string userDomain = null;
string user = username;
if (user.Contains(#"\"))
{
userDomain = user.Substring(0, user.IndexOf("\\"));
user = user.Substring(user.IndexOf("\\") + 1);
}
userDomain = userDomain != null ? userDomain : localDomain;
using (PrincipalContext pc = new PrincipalContext(ContextType.Domain, userDomain, user, password))
{
bool credOk = pc.ValidateCredentials(user, password);
if (!credOk)
return false;
using (UserPrincipal userP = UserPrincipal.FindByIdentity(pc, user))
{
if (userP != null)
{
using (PrincipalContext pc1 = new PrincipalContext(ContextType.Domain, localDomain))
{
using (GroupPrincipal groupPrincipal = new GroupPrincipal(pc1))
{
groupPrincipal.Name = "APP_*";
using (PrincipalSearcher principalSearcher = new PrincipalSearcher(groupPrincipal))
foreach (Principal found in principalSearcher.FindAll())
{
if (found.Name == "APP_Group" && found is GroupPrincipal && userP.IsMemberOf((GroupPrincipal)found))
{
return true;
}
}
}
}
}
}
}
return false;
}
Any idea on why it happens and how to fix it?
It looks like the exception is happening here:
using (PrincipalContext pc = new PrincipalContext(ContextType.Domain, userDomain, user, password))
So do you know what the userDomain being used is? It seems like you might have more than one domain in your environment, so does it happen with all of your domains, or only one?
In our environment, I've seen cases where our AD admins decommission a domain controller, but for some reason that server still shows up in DNS. In other words, if I do a DNS lookup in the command line:
nslookup example.com
one of the IPs is for a decommissioned DC.
That's a possibility in your case. If it picked a bad IP address, then restarting the application would make it do another DNS lookup, which might return a different IP address at the top of the list and things would work again.
To get to the bottom of this, you will really have to observe what is going on at the time it stops working. If you haven't already, install Wireshark on your server. When it stops working, use Wireshark to look for traffic using port 389 (the default LDAP port) and see which IP it's trying to connect to.
Related
I'm writing a program (WinForms, C#) that runs on a Win 7 client machine. It obtains credentials from the user (user id, password, and subdomain name) and uses them to authenticate (via Active Directory) to other servers to which the program remotely connects. The other servers are on a domain different than the domain the Win 7 client machine is on.
Using the NetworkCredential, LdapDirectoryIdentifier, and LdapConnection classes, I can test the credentials with no more than the user id, password, and subdomain name (See answer for S.O. Why does Active Directory validate last password?).
For example, for the user account ssmith#xyz.gov, I need only provide ssmith (user id), the password for ssmith, and xyz (the subdomain). I don't need to provide the top-level domain name (gov in this case).
Now, I want to programmatically obtain the top-level domain name for this user account (gov in this case). I've examined the properties and methods of the NetworkCredential, LdapDirectoryIdentifier, and LdapConnection classes. I've looked over the other classes in the System.DirectoryServices.Protocols Namespace. I don't see a way to programmatically obtain the top-level domain name.
Given user id, password, and subdomain name, how can I obtain the top-level domain name for a user account?
Here is my code. Given a user account of ssmith#xyz.gov, My call looks like this (asterisks represent a SecureString password)
bool result = ValidateCredentials("ssmith","******", "xyz");
Here is my code for the method.
private const int ERROR_LOGON_FAILURE = 0x31;
private bool ValidateCredentials(string username, SecureString ssPassword, string domain)
{
//suports secure string
NetworkCredential credentials = new NetworkCredential(username, ssPassword, domain);
LdapDirectoryIdentifier id = new LdapDirectoryIdentifier(domain);
using (LdapConnection connection = new LdapConnection(id, credentials, AuthType.Kerberos))
{
connection.SessionOptions.Sealing = true;
connection.SessionOptions.Signing = true;
try
{
// The only way to test credentials on a LDAP connection seems to be to attempt a
// Bind operation, which will throw an exception if the credentials are bad
connection.Bind();
}
catch (LdapException lEx)
{
credentials = null;
id = null;
if (ERROR_LOGON_FAILURE == lEx.ErrorCode)
{
return false;
}
throw;
}
}
credentials = null;
id = null;
return true;
}
After a successful bind, then the full DNS name of the domain will be in the LdapConnection object:
var domain = connection.SessionOptions.DomainName;
In this case, that would be "xyz.gov". If you need just "gov", then you can just take everything after the last dot:
var tld = domain.Substring(domain.LastIndexOf('.') + 1);
I am trying to implement a ChangePassword functionality on ActiveDirectory through the UserPrincipal. The code looks like this:
using System.DirectoryServices.AccountManagement;
private PrincipalContext Context { get; set; }
...
Context = new PrincipalContext(ContextType.Domain,
AdDomain,
AdRoot,
ContextOptions.SimpleBind,
AdUsername,
AdPassword)
...
public bool ChangePassword(string login, string password, string newPassword, out string message)
{
using (var foundUser = UserPrincipal.FindByIdentity(Context, IdentityType.SamAccountName, login))
{
try
{
foundUser.ChangePassword(password, newPassword);
foundUser.Save();
}
catch (Exception e)
{
message = e.Message;
return false;
}
return true;
}
}
When I try to test this on my Windows 10 machine I get an exception on ChangePassword, System.Runtime.InteropServices.COMException - One or more input parameters are invalid.
However, when I run this exact same code in the same project while connecting to the same AD domain on my Windows 7 machine it runs without error and changes the password. What could be causing this different behavior on the environments, and why does that error happen?
When the Context is created, make sure to set the ContextOptions to ContextOptions.Negotiate .
If you have mentioned ContextOptions.SimpleBind SetPassword may not work.
PrincipalContext oPrincipalContext = new PrincipalContext
(ContextType.Domain, "Name", "DefaultOU(if required)", ContextOptions.Negotiate,
"Service Account(if required)", "Service password");
i m trying to connect to Active Directory code that i have used
string domain = "domain.com.pk";
string container = "DC=mycompnay,DC=com,DC=pk";
string Admin = "salman.zafar";
string Password = "password";
using (PrincipalContext pc = new PrincipalContext(ContextType.Domain, domain, container, Admin, Password))
{
string userPrincipalName = "dotnettest" + "#" + domain;
// validate the credentials
bool isValid = pc.ValidateCredentials(userPrincipalName, "Ascertia 12");
if (isValid) {
UserPrincipal up = UserPrincipal.FindByIdentity(pc, IdentityType.UserPrincipalName, userPrincipalName);
}
code works fine when the code running on machine which is in domain but if i try to connect to the AD machine that is remote then i get error
i tried to use
string domain = "192.168.0.150:389/domain.com.pk";
then it didn't work and validate credentials method always return false can some one help me how can i connect to remote active directory using IP with port with PrincipalContext or i have to use directory entry
any help will be appreciated
First note:
code works fine when the code running on machine which is in domain
In this case, you do not need to provide adminuser+pw in the PrincipalContext constructor if the machine is a domain member (which I assume here).
If you want to connect to any other AD server (domain controller) with no trust between the foreign domain and the current domain, use the IP address or server name as the "domain" name:
string domain = "192.168.0.150";
If your goal is to just check if credentials are valid, you can even omit the admin user + pw:
string domainController = "192.168.0.150";
using (PrincipalContext pc = new PrincipalContext(ContextType.Domain, domainController))
{
string userPrincipalName = "dotnettest" + "#" + domain;
// validate the credentials
bool isValid = pc.ValidateCredentials(userPrincipalName, "Ascertia 12");
}
In this case, however, you cannot have
UserPrincipal up = UserPrincipal.FindByIdentity(...
because the PrincipalContext itself is not logged on.
You can also see my answer in a similar question: https://stackoverflow.com/a/28690682/4547223
or this SO article Validate a username and password against Active Directory?
I'm trying to connect to AD sever using C#. This is my first time playing with AD.Domain I need to connect to is abc.def.com.
This is a ASP.NET web site and it gives this error. But I can log in to same domain using "ldp.exe" by using same credential. Anyone have idea?
[DirectoryServicesCOMException (0x8007052e): Logon failure: unknown user name or bad "password.
]
System.DirectoryServices.DirectoryEntry.Bind(Boolean throwIfFail) +387825
System.DirectoryServices.DirectoryEntry.Bind() +36
System.DirectoryServices.DirectoryEntry.get_AdsObject() +31
This is my code
static System.DirectoryServices.DirectoryEntry createDirectoryEntry()
{
System.DirectoryServices.DirectoryEntry ldapConnection = new System.DirectoryServices.DirectoryEntry("13.18.12.16", "Administrator", "admin123");
ldapConnection.Path = "LDAP://ou=Users,dc=abc,dc=def,dc=com";
ldapConnection.AuthenticationType = System.DirectoryServices.AuthenticationTypes.Secure;
return ldapConnection;
}
System.DirectoryServices.DirectoryEntry sgscAd = createDirectoryEntry();
System.DirectoryServices.DirectorySearcher search = new System.DirectoryServices.DirectorySearcher(sgscAd);
search.Filter = "(cn=" + m_username + ")";
System.DirectoryServices.SearchResult result = search.FindOne();
The LDAP path to the users container is not correct.
The users container is not an organizational unit but a simple container.
So, you have to specify a different LDAP path.
The LDAP path to the users container in your case is:
LDAP://cn=Users,dc=abc,dc=def,dc=com
Also consider what Hall72215 mentioned in his answer. Use the whole LDAP path directly in the constructor of the DirectoryEntry class.
Why are you giving one path in the constructor (13.18.12.16), and another by setting the Path property? Have you tried giving all of the information in the constructor?
static DirectoryEntry createDirectoryEntry()
{
string username = "Administrator";
string password = "admin123";
string path = "LDAP://13.18.12.16/OU=Users,DC=abc,DC=def,DC=com";
AuthenticationTypes authType = AuthenticationTypes.Secure | AuthenticationTypes.ServerBind;
return new DirectoryEntry(path, username, password, authType);
}
Is Administrator a user in the domain of the domain controller at 13.18.12.16?
I'm a total newbie to this
Trying to connect to an ldap server with PrincipalContext. I have tried all solutions on this site to no avail.
Things I've tried:
PrincipalContext insPrincipalContext =
new PrincipalContext(ContextType.Domain);
PrincipalContext insPrincipalContext =
new PrincipalContext(ContextType.Domain, "ldap://localhost:389/dc=maxcrc,dc=com");
PrincipalContext insPrincipalContext =
new PrincipalContext(ContextType.Domain, "maxcrc.com");
All give the same result:
LDAP server not available
Only ContextType.Machine works basically.
Not sure if my LDAP server is set up correctly:
Host: localhost
Port: 389
Base DN: dc=maxcrc,dc=com
URL: ldap://localhost:389/dc=maxcrc,dc=com
Testing with Softerra LDAP Browser
Any tutorials from start to finish will be much appreciated...
I have been facing the same issue and I found a solution.
I'm able to connect easily using following code:
ADUser_Id = "domainName\\username"; //make sure user name has domain name.
Password = "xxxx";
var context = new PrincipalContext(ContextType.Domain,"server_address", ADUser_Id,Password);
/* server_address = "192.168.15.36"; //don't include ldap in url */
I had similar issues. It turned out that I had to pass username and password in the object initialization. Please try using a statement like below:
PrincipalContext insPrincipalContext =
new PrincipalContext(ContextType.Domain,
"ldap://localhost:389/dc=maxcrc,dc=com",
userName,
password);
Also make sure that your username has domain in it.
For example,
userName = "mydomainname" + "\\" + "john_jacobs"
Use the following constructor overload for PrincipalContext:
public PrincipalContext(
ContextType contextType,
string name,
string container
)
And separate the server name from the LDAP string:
PrincipalContext insPrincipalContext =
new PrincipalContext(ContextType.Domain, "localhost:389", "dc=maxcrc,dc=com");
https://msdn.microsoft.com/en-us/library/bb348316%28v=vs.110%29.aspx
In my environment I had to create the principal context with just the domain controller host name, and then separately validate the user credentials.
string domainControllerName = "PDC";
string domainName = "MyDomain"; // leave out the .Local, this is just to use as the prefix for the username if the user left it off or didn't use the principal address notation
string username = "TestUser";
string password = "password";
using (var ldap = new PrincipalContext(ContextType.Domain, domainControllerName))
{
var usernameToValidate = username;
if (!usernameToValidate.Any(c => c == '#' || c == '\\'))
usernameToValidate = $"{domainName}\\{username}";
if (!ldap.ValidateCredentials(username, context.Password, ContextOptions.SimpleBind))
throw new UnauthorizedException();
}
This example allows for all three of these variations to the username to validate:
TestUser
MyDomain\TestUser
TestUser#MyDomain.Local
You may want to try your local machine address instead :
ldap://127.0.0.1:389/dc=maxcrc,dc=com
If that doesn't work, I'd fire up Wireshark, and have it capture traffic on port 389 as you're attempting to connect via Softerra.
In my time working with LDAP and .Net DirectoryServices, that error usually means the syntax or naming convention of the path is incorrect, or does not point to a valid directory end point.
That error might be due to trying to connect as "Anonymous" without specifying it explicitly.
By default all connections are Negotiable. So if you try something like that you could try the following:
LdapDirectoryIdentifier ldap = new LdapDirectoryIdentifier("My Hostname or IP Address",10389); //10389 might be your non default port
LdapConnection connection = new LdapConnection(ldap);
connection.AuthType = AuthType.Anonymous;