Use SSL to connect to LDAP with .Net DirectoryServices - c#

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.

Related

LDAP reset password from outside the domain network C# Error: RPC server is unavailable. (exception from hresult: 0x800706ba)

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.

LDAPS connection with ASP.Net/C#

I have a connection string for LDAP protocol
ldap://ldap.example.com:636/DC=users,DC=buyers
which works fine.
But I need to use a LDAPS connection :
ldaps://ldap.example.com/DC=users,DC=buyers
which does show up in ldp.exe windows form when I test the connection.
Unfotunately it does not work in the Asp.Net application. I get "Unknown error (0x80005000)".
I am not sure whether LDAPS string is even possible with Asp.Net. I downloaded the source code into LDAPConnection.cs class and was unable to find any valuable information.
The method you found that works is indeed using LDAPS:
ldap://ldap.example.com:636/DC=users,DC=buyers
That's the only way to do it. I do that in one of my existing projects. It doesn't understand "LDAPS://".
If you don't believe me :) fire up Wireshark as you debug. When it connects, you'll see the SSL handshake to your domain controller.
Port 636 is only for LDAPS. Port 389 is the non-SSL port.
If you have more than one domain, you can use port 3269 for the global catalog via SSL.
Below code worked for me to connect to AD using LDAPS
ldapConnection = new LdapConnection(new LdapDirectoryIdentifier("your.LDAPSserver.com", 636));
var networkCredential = new NetworkCredential("UsernameWithoutDomain", "yourPassword", "AD.yourDOMAIN.com");
ldapConnection.SessionOptions.SecureSocketLayer = true;
ldapConnection.SessionOptions.ProtocolVersion = 3;
ldapConnection.SessionOptions.VerifyServerCertificate = new VerifyServerCertificateCallback(ServerCallback);
ldapConnection.AuthType = AuthType.Negotiate;
ldapConnection.Bind(networkCredential);
SearchRequest Srchrequest = new SearchRequest("CN=Users,DC=AD,DC=YOURCOMPANY,DC=COM", "mail=useremail#company.com", System.DirectoryServices.Protocols.SearchScope.Subtree);
SearchResponse SrchResponse = (SearchResponse)ldapConnection.SendRequest(Srchrequest);
// ServerCallback
private static bool ServerCallback(LdapConnection connection, X509Certificate certificate)
{
return true;
}
Surprisingly it is also working when I am not using networkCredential and just using ldapConnection.Bind(); Seems it is using my local credentials as default on my local machine.

Connect to Active Directory using LdapConnection class on remote server

I have a problem: I need to connect from a remote server to Active Directory, but the code has to be using the LdapConnection class. I need this because that way I can only test change notifiers when some event happen (such as user is deactivated or he changed group, data etc). OS on the remote server is Windows Server 2012.
I managed to do this from local using DirectoryServices with the following code:
String ldapPath = "LDAP://XRMSERVER02.a24xrmdomain.info";
directoryEntry = new DirectoryEntry(ldapPath, #"A24XRMDOMAIN\username", "pass");
//// Search AD to see if the user already exists.
DirectorySearcher search = new DirectorySearcher(directoryEntry);
search.Filter = "(&(objectClass=user))";
SearchResult result = search.FindOne();
This is okay and connection works but now I need to connect using the LdapConnection class.
I tried something like this on many ways but none of that helped me:
LdapConnection connection = new LdapConnection(XRMSERVER02.a24xrmdomain.info);
var credentials = new NetworkCredential(#"A24XRMDOMAIN\username", "pass");
connection.Credential = credentials;
connection.Bind();
It says that credentials are invalid but that is not true.
Explanations:
XRMSERVER02 - Domain controller
a24xrmdomain.info - Domain
A24XRMDOMAIN - Domain used for logging
Thanks for your help.
Even though I solved my problem I want to share with other developers what I achieved so far. Problem that I encountered was that I had remote server with OS Windows server 2012 and Active directory on it. I needed to connect on him via my local machine(Windows 10).
As I stated in my question it is possible to do that via DirectoryServices with the following code:
String ldapPath = "LDAP://(DomainController).a24xrmdomain.info";
directoryEntry = new DirectoryEntry(ldapPath, #"DOMAIN\username","pass");
//// Test search on AD to see if connection works.
DirectorySearcher search = new DirectorySearcher(directoryEntry);
search.Filter = "(&(objectClass=user))";
SearchResult result = search.FindOne();
This is one of the solutions, but since my task was to get notification and to identify when ever some object has changed in Active Directory, I needed connection to Active Directory on Remote server via LDAP class. Code for getting notifiers is taken from:
- Registering change notification with Active Directory using C#
I succeeded to connect with LDAP class via next code:
String ldapPath2 = "(DomainController).a24xrmdomain.info";
LdapConnection connection = new LdapConnection(ldapPath2);
var credentials = new NetworkCredential(#"username", "pass");
connection.Credential = credentials;
connection.Bind();
Want to mention that no IP address of remote server is needed, just Domain Controller that is used on him, and that Domain used for logging is unnecessary.
Happy coding
Try using NetworkCredential constructor with 3 parameters: username, password and domain. Specify domain separately from user name

How to know that Active Directory exists with only ip address?

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.

How do I validate Active Directory creds over LDAP + SSL?

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.

Categories

Resources