I'm trying to use the .NET 3.5 System.DirectoryServices.AccountManagement namespace to validate user credentials against our Active Directory LDAP server over an SSL encrypted LDAP connection. Here's the sample code:
using (var pc = new PrincipalContext(ContextType.Domain, "sd.example.com:389", "DC=sd,DC=example,DC=com", ContextOptions.Negotiate))
{
return pc.ValidateCredentials(_username, _password);
}
This code works fine over unsecured LDAP (port 389), however I'd rather not transmit a user/pass combination in clear text. But when I change to LDAP + SSL (port 636), I get the following exception:
System.DirectoryServices.Protocols.DirectoryOperationException: The server cannot handle directory requests.
at System.DirectoryServices.Protocols.ErrorChecking.CheckAndSetLdapError(Int32 error)
at System.DirectoryServices.Protocols.LdapSessionOptions.FastConcurrentBind()
at System.DirectoryServices.AccountManagement.CredentialValidator.BindLdap(NetworkCredential creds, ContextOptions contextOptions)
at System.DirectoryServices.AccountManagement.CredentialValidator.Validate(String userName, String password)
at System.DirectoryServices.AccountManagement.PrincipalContext.ValidateCredentials(String userName, String password)
at (my code)
Port 636 works for other activities, such as looking up non-password information for that LDAP/AD entry...
UserPrincipal.FindByIdentity(pc, IdentityType.SamAccountName, _username)
...so I know it's not my LDAP server's SSL setup, since it works over SSL for other lookups.
Has anyone gotten the ValidateCredentials(...) call to work over SSL? Can you explain how? Or is there another/better way to securely validate AD/LDAP credentials?
I was able to validate credentials using the System.DirectoryServices.Protocols namespace, thanks to a co-worker. Here's the code:
// See http://support.microsoft.com/kb/218185 for full list of LDAP error codes
const int ldapErrorInvalidCredentials = 0x31;
const string server = "sd.example.com:636";
const string domain = "sd.example.com";
try
{
using (var ldapConnection = new LdapConnection(server))
{
var networkCredential = new NetworkCredential(_username, _password, domain);
ldapConnection.SessionOptions.SecureSocketLayer = true;
ldapConnection.AuthType = AuthType.Negotiate;
ldapConnection.Bind(networkCredential);
}
// If the bind succeeds, the credentials are valid
return true;
}
catch (LdapException ldapException)
{
// Invalid credentials throw an exception with a specific error code
if (ldapException.ErrorCode.Equals(ldapErrorInvalidCredentials))
{
return false;
}
throw;
}
I'm not thrilled with using a try/catch block to control decisioning logic, but it's what works. :/
Maybe this is another way. There's nothing unusual in validate credentials. The ContextOptions must set properly.
Default value:
ContextOptions.Negotiate | ContextOptions.Signing | ContextOptions.Sealing
Add Ssl:
ContextOptions.Negotiate | ContextOptions.Signing | ContextOptions.Sealing | ContextOptions.SecureSocketLayer
ContextOptions.Negotiate or ContextOptions.SimpleBind is required. Or whatever your server need to perform authentication. ContextOptions only supports OR bit to bit.
You could try also set the ContextOptions directly this way in ValidateCredentials method.
using (var pc = new PrincipalContext(ContextType.Domain, "sd.example.com:636", "DC=sd,DC=example,DC=com", ContextOptions.Negotiate | ContextOptions.SecureSocketLayer))
{
return pc.ValidateCredentials(_username, _password);
}
Or
using (var pc = new PrincipalContext(ContextType.Domain, "sd.example.com:636", "DC=sd,DC=example,DC=com", ContextOptions.Negotiate))
{
return pc.ValidateCredentials(_username, _password, ContextOptions.Negotiate | ContextOptions.SecureSocketLayer);
}
For me, the ValidateCredentials method works just fine. The problem, I found, was on the server hosting the AD (I'm using AD LDS). You needed to associate the server certificate with the AD instance. So if your instance was called 'MyAD' (or ActiveDirectoryWebService), you needed to open up the MMC, snap in the 'Certificates' module, select 'Service Account' and then select 'MyAD' from the list. From there you can add the SSL certificate into the 'MyAD' Personal store. This finally kicked the SSL processing into gear.
I suspect, from what I know of the LdapConnection method and the fact that you omitted the callback function, that you are not validating your server certificate. It's a messy job and ValidateCredentials does it for free. Probably not a big deal, but a security hole none-the-less.
I know this is old, but for anybody running into this again:
PrincipalContext.ValidateCredentials(...), by default, tries to open an SSL connection (ldap_init(NULL, 636)) followed by setting the option LDAP_OPT_FAST_CONCURRENT_BIND.
If a (trusted?) client certificate is present, however, the LDAP connection is implicitly bound and fast bind cannot be enabled anymore. PrincipalContext doesn't consider this case and fails with an unexpected DirectoryOperationException.
Workaround: To support SSL where possible, but have a fallback, call ValidateCredentials(...) with default options first (i.e. no options). If this fails with the DirectoryOperationException, try again by specifying the ContextOptions (Negotiate | Sealing | Signing), which is what ValidateCredentials internally does for the expected LdapException anyway.
Related
We're trying to Reset LDAP password, its working on development environment but not working on production environment.
Our development environment is inside the Domain and production environment is outside the Domain.
In development to connect LDAP, we have used Domain name like abc.com and production environment we use IPaddress:389, which is already working for LDAP User authentication in both environment. But not working for LDAP reset password.
Error: RPC server is unavailable. (exception from hresult: 0x800706ba)
Developement: (working)
PrincipalContext principalContext =
new PrincipalContext(ContextType.Domain, "<domain.com>", container: "<DC=domain,DC=com>",
"<username>", "<password>");
UserPrincipal user = UserPrincipal.FindByIdentity(principalContext, "<LdapUserName>");
// "<username>", "<password>" are Administrative credential.
bool isValid = user.ValidateCredentials("<username>", "<password>");
_logger.Log($"Is Connection: {isValid}");
**// Output: Is Connection: True**
user.UserCannotChangePassword = false;
user.SetPassword("<NewPassword>");
// Password has been successfully reset.
Production: (working)
Also we are authenticate LDAP users using below method its working on Production:
Check user has LDAP account or not:
// "<username>", "<password>" are Administrative credential.
var entry = new DirectoryEntry($"LDAP://{"<IP:389>"}", "<username>", "<password>",
AuthenticationTypes.Secure | AuthenticationTypes.Sealing | AuthenticationTypes.ServerBind);
var search = new DirectorySearcher(entry);
var strFilter = $"(mail={"<UserEmailId>"})";
search.Filter = strFilter;
var result = await Task.Run(() => search.FindOne());
if (result != null)
{
//IsLdapUser = true;
//result.Properties["samaccountname"][0]);
}
else
{
//IsLdapUser = false;
}
// Successfully
// Authenticate LDAP user:
var ldapConnection = new LdapConnection(new LdapDirectoryIdentifier("<IP:389>", false, false));
var nc = new NetworkCredential("<LdapUserName>", "<LdapUserPassword>", "<IP:389>");
ldapConnection.Credential = nc;
ldapConnection.AuthType = AuthType.Negotiate;
ldapConnection.Bind(nc);
// Successfully
Production: (not working)
// "<username>", "<password>" are Administrative credential.
PrincipalContext principalContext =
new PrincipalContext(ContextType.Domain, "<IP:389>", container: "<DC=domain,DC=com>",
"<username>", "<password>");
UserPrincipal user = UserPrincipal.FindByIdentity(principalContext, "<LdapUserName>");
bool isValid = user.ValidateCredentials("<username>", "<password>");
_logger.Log($"Is Connection: {isValid}");
**// Output: Is Connection: True**
user.UserCannotChangePassword = false;
user.SetPassword("<NewPassword>");
// Error: RPC server is unavailable. (exception from hresult: 0x800706ba)
Also tried with below code (not working)
// "<username>", "<password>" are Administrative credential.
DirectoryEntry de = new DirectoryEntry("<IP:389>","<username>", "<password>",
AuthenticationTypes.Secure | AuthenticationTypes.Sealing | AuthenticationTypes.ServerBind);
// LDAP Search Filter
DirectorySearcher ds = new DirectorySearcher(de);
ds.Filter = "(&(objectClass=user)(|(sAMAccountName=" + "<LdapUserName>"+ ")))";
// LDAP Properties to Load
ds.PropertiesToLoad.Add("displayName");
ds.PropertiesToLoad.Add("sAMAccountName");
ds.PropertiesToLoad.Add("DistinguishedName");
ds.PropertiesToLoad.Add("CN");
// Execute Search
SearchResult result = await Task.Run(() => ds.FindOne());
string dn = result.Properties["DistinguishedName"][0].ToString();
DirectoryEntry uEntry = result.GetDirectoryEntry();
uEntry.Invoke("SetPassword", new object[] { "<NewPassword>"}); //Set New Password
uEntry.CommitChanges();
uEntry.Close();
// Error: RPC server is unavailable. (exception from hresult: 0x800706ba)
The attribute used to modify the password is unicodePwd. That documentation reveals some conditions that must be met for the password to be changed. Primarily, the connection must be encrypted.
Calling .Invoke("SetPassword", ...) actually calls the native Windows IADsUser::SetPassword method. That documentation shows that it automatically attempts a few different ways to encrypt. The exception happens because none of these methods worked.
You can actually modify the unicodePwd attribute directly, without calling SetPassword, which I'll get to, but regardless, you have to resolve the issue of encryption first.
When you're running this from a computer inside the network, AuthenticationTypes.Sealing is enough. As the documentation says, the effect is that it uses Kerberos to encrypt the connection.
But when you're connecting from outside the domain, Kerberos won't work (maybe it will with effort - I'm no Kerberos expert). So the only usable encryption method is SSL. The SetPassword method does actually attempt to use SSL, but clearly it didn't work.
One problem I see right away is that you're using an IP address to connect to the DC, and SSL won't work using an IP address, since the domain name name on the SSL certificate must match the name you are using to access to the server, and the SSL cert will not have the IP address on it. So you will have to change that to use the domain name. If DNS will not resolve the name, you can add it to your hosts file.
Changing that may fix everything. If not, there can be two other issues:
Port 636 is not accessible (a firewall in the way).
The SSL certificate is not trusted on the computer you run this on
LDAP over SSL (LDAPS) works on port 636. You can test this connection in PowerShell:
Test-NetConnection example.com -Port 636
If that fails, fix that first.
Next, check the certificate. You can download the certificate with this PowerShell script:
$webRequest = [Net.WebRequest]::Create("https://example.com:636")
try { $webRequest.GetResponse() } catch {}
$cert = $webRequest.ServicePoint.Certificate
$bytes = $cert.Export([Security.Cryptography.X509Certificates.X509ContentType]::Cert)
set-content -value $bytes -encoding byte -path "certificate.cer"
Change the example.com in the first line to your domain name (leave the https:// and :636). Then you will have a file called certificate.cer in the current directory that you can open and inspect. It will warn you if it is not trusted. If it's not trusted, then you will have to install the root certificate on the server as a Trusted Root Certificate.
If it is already trusted, make sure the "Issued to:" domain name on the cert matches the name you used to connect. In our environment, the SSL certificates are in the name of each domain controller (dc1.example.com), not the domain name (example.com). So I have to target a specific domain controller for LDAPS to work.
Once you get all of that sorted out, your code should work.
If you want to change the unicodePwd attribute directly instead of using SetPassword (which may or may not perform a little faster), you will need to make the original connection via SSL. For example:
DirectoryEntry de = new DirectoryEntry("LDAP://dc1.example.com:636","<username>", "<password>",
AuthenticationTypes.Secure | AuthenticationTypes.SecureSocketsLayer | AuthenticationTypes.ServerBind);
Only use AuthenticationTypes.ServerBind if you are targeting a specific DC.
Then you can update the unicodePwd attribute in the very specific way that it wants:
uEntry.Properties["unicodePwd"].Value = Encoding.Unicode.GetBytes("\"NewPassword\"");
uEntry.CommitChanges();
Note that the new password must be enclosed in quotes.
I am trying to run my password change application from a non domain joined machine. The code works fine when run from domain joined machine. So now, I am connecting to the AD with direct LDAP connection via SSL. After changepassword method is invoked, I am getting an error:
Configuration information could not be read from the domain controller, either because the machine is unavailable, or access has been denied. (Exception from HRESULT: 0x80070547).
I am making the connection and running the application using a service account with permission to change user passwords.
string adminUser = Domain + #"\" + AdminUserName;
string adminPass = AdminUserPassword;
string ldapString = LDAPString;
DirectoryEntry de = new DirectoryEntry(ldapString, adminUser, adminPass, AuthenticationTypes.Secure);
DirectorySearcher deSearch = new DirectorySearcher(de) { SearchRoot = de, Filter = "(&(objectCategory=user)(cn=" + userName + "))" };
SearchResult result = deSearch.FindOne();
if (result != null)
{
var adContext = new PrincipalContext(ContextType.Domain);
currentdc = adContext.ConnectedServer;
DirectoryEntry userEntry = result.GetDirectoryEntry();
if (userEntry != null)
{
userEntry.Invoke("ChangePassword", new object[] { OldPassword, NewPassword });
}
}
Invoking ChangePassword, calls IADsUser::ChangePassword. That documentation says it works much the same as IADsUser::SetPassword. That documentation has more information. Really, only the first method would work when you're running this from outside the domain:
First, the LDAP provider attempts to use LDAP over a 128-bit SSL connection. For LDAP SSL to operate successfully, the LDAP server must have the appropriate server authentication certificate installed and the clients running the ADSI code must trust the authority that issued those certificates. Both the server and the client must support 128-bit encryption.
I assume your LDAPString is in the format LDAP://example.com:636 (the :636 being the important part). If you can read data like that, then the SSL certificate is trusted. So that's good.
The only maybe missing piece could be 128-bit encryption? Check the certificate and see if it's maybe using less than 128-bit. Although I'd be surprised if it did.
This answer has a short snippet of code that you can use to download a certificate from any site: https://stackoverflow.com/a/22251597/1202807
Just use "https://example.com:636" as the "website".
There is also this:
In Active Directory, the caller must have the Change Password extended control access right to change the password with this method.
You should make sure that the user account you are authenticating to LDAP with does have the Change Password permission on the account you are trying to update. In our environment, Everyone has the Change Password permission (since you still need to provide the old password to do it). I think that's the default, but it's worth checking.
I have some .NET working code (both as a desktop application and as a IIS deployment) to read data from LDAP:
string ldapUrl = "LDAP://myLdapUrl.example/ou=user,dc=MyDC";
AuthenticationTypes auth = AuthenticationTypes.None;
using (DirectoryEntry directoryEntry = new DirectoryEntry(
ldapUrl,
"cn=ldap_user,ou=user,dc=MyDC",
"NotMyTruePassword",
auth)
{
using (DirectorySearcher directorySearcher = new DirectorySearcher(directoryEntry))
{
directorySearcher.PropertiesToLoad.AddRange(new[] { "uid", "givenname", "sn", "middlename", "description", "memberof" });
directorySearcher.Filter = String.Format("(&(objectclass=person)(cn={0}))", user);
directorySearcher.SearchScope = SearchScope.OneLevel;
directorySearcher.SizeLimit = 10;
SearchResult searchResult = directorySearcher.FindOne();
}
}
But when I try to connect to the LDAPS port (636), it fails with a
System.Runtime.InteropServices.COMException (0x8007203A): Server is not operational.
Considerations:
I have added the server CA to my acount through MMC.
After that, I can connect to the LDAPS port using LdapAdmin.
I have tried the following changes:
Just adding the port to the server URL1:
string ldapUrl = "LDAP://myLdapUrl.example:636/ou=user,dc=MyDC";
Adding the port and changing the authTypes to SecureSocketsLayer2:
string ldapUrl = "LDAP://myLdapUrl.example:636/ou=user,dc=MyDC";
AuthenticationTypes auth = AuthenticationTypes.SecureSocketsLayer;
Adding the port and changing the authType to Secure2:
string ldapUrl = "LDAP://myLdapUrl.example:636/ou=user,dc=MyDC";
AuthenticationTypes auth = AuthenticationTypes.Secure;
And I always get the same results.
I have found some examples using directly the LDAP connections (from System.DirectoryServices.Protocols) but I would prefer not to change the code as I already got it working.
1 I often see some people claiming that I should change LDAP:// for LDAPS:, but it seems that it is not how DirectoryServices works. And in any case that fails, too.
2 I am pretty sure those two options are for authentication and not for setting up the SSL connection, but I have tried them anyway.
For security oriented connection, we cannot use the AuthenticationType.None.
Could you try with the below one.
AuthenticationTypes authType = AuthenricationTypes.Secure;
Have you confirmed it's not a network issue?
From PowerShell you can use this to test the connection:
Test-NetConnection myLdapUrl.example -Port 636
If that works, then it's possible that the certificate from your server is not trusted. You can use this PowerShell code to download the certficate into a .cer file that you can open and inspect:
$webRequest = [Net.WebRequest]::Create("https://myLdapUrl.example:636")
try { $webRequest.GetResponse() } catch {}
$cert = $webRequest.ServicePoint.Certificate
$bytes = $cert.Export([Security.Cryptography.X509Certificates.X509ContentType]::Cert)
set-content -value $bytes -encoding byte -path "$home\Downloads\myLdapUrl.example.cer"
That will save myLdapUrl.example.cer to your Downloads folder. Double-click on it to view it. There will be an obvious warning there if the certificate is not trusted. If that's the case, you need to get the root certificate and install it as a trusted cert on each computer this code will run on.
I have C# Windows Form application to test the LDAP SSL authentication.
Here is the code. First, I made a function.
using System.DirectoryServices;
private bool VerifyDomainUserUsignLDAP(string UserName, string Password, string Domain,string mode, out string message)
{
bool retVal = false;
message = null;
DirectoryEntry de;
try
{ if (mode =="Plain")
//plain mode
de = new DirectoryEntry(Domain, UserName, Password);
else
//SSL mode
de = new DirectoryEntry(Domain, UserName, Password,
AuthenticationTypes.Secure | AuthenticationTypes.SecureSocketsLayer);
DirectorySearcher ds = new DirectorySearcher(de);
SearchResult sr= ds.FindOne();
lblResult.Text = "Authentication Passed! " + sr.ToString();
retVal = true;
}
catch (Exception ex)
{
retVal = false;
lblResult.Text = ex.Message;
}
return retVal;
}
My problem is the invoke.
Share with some background first.
We have multiple domain control servers (Windows). dcserver001.mydomain.com is one of them. (of course, we have dcserver002.mydomain.com, dcserver003.mydomain.com, etc). each server provides LDAPS service.
And we created a VIP name ldap.mydomain.com in F5 (Load balance), we put all above dc servers into the Load balance. All DC servers are Windows servers.
Days before, if I use following line to invoke above function for LDAP authenticate on the VIP name - ldap.mydomain.com.
For e.g.
VerifyDomainUserUsignLDAP("mydomain\\myuserid", "mypassword",
#"LDAP://ldap.mydomain.com", "SSL" ,out Message);
It always worked fine and the user was authenticated.
However, some days before, our LDAP service team made a registry change (LdapEnforceChannelBinding) on each ldap servers to
enhance the security based on MS suggestion.
In short, they changed following key value from 0 to 2
Path: HKEY_LOCAL_MACHINE/System/CurrentControlSet/Services/NTDS/Parameters
Key: LdapEnforceChannelBinding
Here is detail page about the setting on MS web site.
Use the LdapEnforceChannelBinding registry entry to make LDAP authentication over SSL/TLS more secure
https://support.microsoft.com/en-hk/help/4034879/how-to-add-the-ldapenforcechannelbinding-registry-entry
After that, I noticed my above function stop working.
i.e. if I use same line to invoke above function for LDAP authenticate.
For e.g.
VerifyDomainUserUsignLDAP("mydomain\\myuserid", "mypassword",
#"LDAP://ldap.mydomain.com", "SSL" ,out Message);
It always returned exception "Logon failure: unknown user name or password."
(I promise password and user name were correct.)
Then, I did further investigation.
I tried to use following line to invoke above function for LDAP authenticate on any individual dc server, e.g.
dcserver001.mydomain.com.
VerifyDomainUserUsignLDAP("mydomain\\myuserid", "mypassword",
#"LDAP://dcserver001.mydomain.com", "SSL" ,out Message);
It worked fine as well.
I actually tested all individual dc servers one by one, thwy were all working.
So, it looks like the ldap request with same invoke parameters works well on the individual dc server, but it doesn't work on the VIP name.
Then, I asked the ldap server team to rollback to LdapEnforceChannelBinding change to value 0. Then, I re-tested ldap against both individual server and VIP name, both worked.
I checked with our metwork team and got got some more information as follwoing.
They said this won't work with LDAPS VIPs because the SSL channel from client is terminated on F5, and reestablished to DC.
the reason why it works directly to the dc is because its one continuous packet.
The update addresses this vulnerability by incorporating support for Extended Protection for Authentication security feature, which allows the LDAP server to detect and block such forwarded authentication requests once enabled.
What I need help is - is there anyone here encountered the similar ldap ssl logon issue against F5 VIP and with the LdapEnforceChannelBinding registry value = 1 or 2?
If LdapEnforceChannelBinding registry value = 1 or 2 on the LDAP servers, what changes need to be done to resolve above LDAPS logon issue?
Thanks a lot!
Jun
How to know that AD exists?
I have only ip address. I tried to use those methods:
if(DirectoryEntry.Exists("LDAP://192.168.1.1"))
also
DirectoryEntry directoryEntry = new DirectoryEntry("LDAP://192.168.1.1")
but it didn't help.
I use LdapConnection right now, but I have a problem
LdapConnection connection = new LdapConnection(new LdapDirectoryIdentifier("192.168.1.1"));
connection.AuthType = AuthType.Basic;
NetworkCredential credential =
new NetworkCredential("a", '1");
connection.Credential = credential;
connection.Timeout = new TimeSpan(1000);
connection.Bind();
I'm getting 81 code and The LDAP unavailable.
Does somebody know is possible just to know is ip is correct and AD exists?
P.S. I use .NET 2
You can try this (works with .NET 2.0 and does not need credentials):
...
using System.DirectoryServices.Protocols;
...
string server = "192.168.1.1";
using (LdapConnection ldapConnection = new LdapConnection(server))
{
ldapConnection.AuthType = AuthType.Anonymous;
SearchRequest request = new SearchRequest(null, "(objectclass=*)",
SearchScope.Base, "defaultNamingContext");
SearchResponse result = (SearchResponse)ldapConnection.SendRequest(request);
if (result.Entries.Count == 1)
{
Console.WriteLine(result.Entries[0].Attributes["defaultNamingContext"][0]);
}
}
It binds anonymously to the AD domain controller and retrieves the rootDSE entry. It displays the DN of the AD domain.
You can also query another attributes, see https://msdn.microsoft.com/en-us/library/ms684291(v=vs.85).aspx
AD can only be set to run on port 389 and/or 636. So if the port is open, it is a pretty good chance that LDAP is present.
Know if it is AD or not, would, typically, require you to have a valid LDAP account to BIND to the LDAP service.
You can perform a LDAP query against the LDAP service and probably learn the VendorName.