I have the following two implementations of authenticating users with LDAP and LDAPS and I was wondering which was better / more correct. For the record, both of these work on both SSL and non-SSL connections.
I'm also curious because when watching with Wireshark on the Non-SSL PrincipalContext version, I still see traffic on Port 636. Of the four combinations (Non-SSL LdapConnection, SSL LdapConnection, Non-SSL PrincipalContext, SSL PrincipalContext) it is the only one that has traffic on both Port 389 and 636 instead of just one or the other. What could be causing this?
LDAP Connection Method:
bool userAuthenticated = false;
var domainName = DomainName;
if (useSSL)
{
domainName = domainName + ":636";
}
try
{
using (var ldap = new LdapConnection(domainName))
{
var networkCredential = new NetworkCredential(username, password, domainName);
ldap.SessionOptions.VerifyServerCertificate = new VerifyServerCertificateCallback((con, cer) => true);
ldap.SessionOptions.SecureSocketLayer = useSSL;
ldap.SessionOptions.ProtocolVersion = 3;
ldap.AuthType = AuthType.Negotiate;
ldap.Bind(networkCredential);
}
// If the bind succeeds, we have a valid user/pass.
userAuthenticated = true;
}
catch (LdapException ldapEx)
{
// Error Code 0x31 signifies invalid credentials, anything else will be caught outside.
if (!ldapEx.ErrorCode.Equals(0x31))
{
throw;
}
}
return userAuthenticated;
PrincipalContext Method:
bool userAuthenticated = false;
var domainName = DomainName;
if (useSSL)
{
domainName = domainName + ":636";
ContextOptions options = ContextOptions.SimpleBind | ContextOptions.SecureSocketLayer;
using (PrincipalContext pc = new PrincipalContext(ContextType.Domain, domainName, null, options))
{
userAuthenticated = pc.ValidateCredentials(username, password, options);
}
}
else
{
using (PrincipalContext pc = new PrincipalContext(ContextType.Domain, domainName))
{
userAuthenticated = pc.ValidateCredentials(username, password);
}
}
return userAuthenticated;
#DTI-Matt, in the examples above, you use VerifyServerCertificate callback that always returns true. This, essentially, defies the purpose of connecting to LDAP over SSL, as no real certificate check is performed.
While you could implement a real certificate check using X509Chain and/or X509Certificate2 classes, it seems PrincipalContext handles the checks for you.
To summarize, both LdapConnection and PrincipalContext provide very similar functionality, in means of connecting to an LDAP server over plain or SSL connection. You have to supply LdapConnection much more hand-written code for it to work properly. On the other hand, PrincipalContext gives you the same functionality with less code to write by hand.
As a note, connections to port 636 (your default LDAP over SSL port), by non-SSL PrincipalContext may be explained by the fact this class tries to connect as secure as possible.
This is what we ended up with that is working over SSL/Non-SSL.
public bool UserValid(string username, string password, bool useSSL)
{
bool userAuthenticated = false;
var domainName = DomainName;
if (useSSL)
{
domainName = domainName + ":636";
}
try
{
using (var ldap = new LdapConnection(domainName))
{
var networkCredential = new NetworkCredential(username, password, DomainName); // Uses DomainName without the ":636" at all times, SSL or not.
ldap.SessionOptions.VerifyServerCertificate += VerifyServerCertificate;
ldap.SessionOptions.SecureSocketLayer = useSSL;
ldap.AuthType = AuthType.Negotiate;
ldap.Bind(networkCredential);
}
// If the bind succeeds, we have a valid user/pass.
userAuthenticated = true;
}
catch (LdapException ldapEx)
{
// Error Code 0x31 signifies invalid credentials, so return userAuthenticated as false.
if (!ldapEx.ErrorCode.Equals(0x31))
{
throw;
}
}
return userAuthenticated;
}
private bool VerifyServerCertificate(LdapConnection connection, X509Certificate certificate)
{
X509Certificate2 cert = new X509Certificate2(certificate);
if (!cert.Verify())
{
// Could not validate potentially self-signed SSL certificate. Prompting user to install certificate themselves.
X509Certificate2UI.DisplayCertificate(cert);
// Try verifying again as the user may have allowed the certificate, and return the result.
if (!cert.Verify())
{
throw new SecurityException("Could not verify server certificate. Make sure this certificate comes from a trusted Certificate Authority.");
}
}
return true;
}
Related
I'm currently trying to connect to a LDAPS Server using the following VB.NET Code, which should set the right parameters and use the function seen below to verify the certificate when the Bind function is called.
The value of LdapHost is the IP, 10.100.11.10, and the value for LdapPort is 7636.
connection = new LdapConnection(new LdapDirectoryIdentifier(LdapHost, LdapPort));
connection.AuthType = 2; // Negotiate
connection.SessionOptions.SecureSocketLayer = true;
connection.SessionOptions.VerifyServerCertificate = new VerifyServerCertificateCallback(VerifyServerCertificate);
//Both username and password are correct
connection.Credential = new System.Net.NetworkCredential(strUsername, strPassword);
connection.Bind();
This,
But upon trying to verify the Server Certificate, using the following code:
private bool VerifyServerCertificate(LdapConnection ldapConnection, X509Certificate certificate)
{
try
{
X509Certificate2 certificate2 = new X509Certificate2(certificate);
return certificate2.Verify();
}
catch (Exception ex)
{
throw new LdapException(9999, "Invalid certificate or path.");
}
}
It Errors out at the Bind function saying that it cannot connect to the LDAP Server at all with the message "The LDAP Server cannot be reached"
Although upon testing the connection via PowerShell, the Server is available just fine.
Is there something wrong with my verification method? Should I try a different approach entirely?
I have found the reason why the verification did not work.
Using
X509Chain chain = new X509Chain();
X509Certificate2 certificate2 = new X509Certificate2(certificate);
var chainBuilt = chain.Build(certificate2);
LogEvent("Val", 0, "Chain building status: " + chainBuilt);
if (chainBuilt == false) {
foreach (X509ChainStatus chainStatus in chain.ChainStatus)
LogEvent("Val", 0, "Chain error: " + chainStatus.Status + " " + chainStatus.StatusInformation);
chain.Reset();
return false;
} else {
chain.Reset();
return true;
}
if the verification fails helped me understand that the Root Certificate was not trusted on that specific server.
Furthermore, it told me that it could not reach the Revokation Server to check if the Certificate is still valid.
This couldn't be checked though, since the configuration uses a StartTLS certificate, which does not have a Revokation Server.
Therefore, I added
chain.ChainPolicy.VerificationFlags = X509VerificationFlags.IgnoreRootRevocationUnknown | X509VerificationFlags.IgnoreEndRevocationUnknown | X509VerificationFlags.IgnoreCtlSignerRevocationUnknown;
to ignore every property regarding the Revokation Server. It can now connect as intended.
I struggle with LDAPConnection. Does anybody has a good example on how to use LDAPConnection properly? Google doesnt give me something I could work with.
I need to open e LDAP-Connection using SSl and having a Certificate + PW.
// options
string sServer = "LDAP://192.168.50.147:636/dc=data,dc=vzd";
// ldap
LdapDirectoryIdentifier ldapIdent = new LdapDirectoryIdentifier(server: "192.168.50.147", portNumber: 636, fullyQualifiedDnsHostName: true, connectionless: false);
NetworkCredential netCred = new NetworkCredential(myUserName, myCertificate);
// ldap connection
LdapConnection ldapConn = new LdapConnection(ldapIdent);
ldapConn.SessionOptions.SecureSocketLayer = true;
ldapConn.AuthType = AuthType.Negotiate;
ldapConn.SessionOptions.VerifyServerCertificate += delegate { return true; };
ldapConn.ClientCertificates.Add(clsGlobal.TI_Steuer.certX509);
ldapConn.Bind(netCred);
// query
string sQueryFormat = "(&(sn=*e*))"
My questions are:
How do I exactly connect to "LDAP://192.168.50.147:636/dc=data,dc=vzd"?
How do I attach Certificate + Certificate-PW with SSL to that Request?
How do I add a SearchQuery?
How do I get the LDAP's answer?
Thanks in advance. I appreciate every help.
I want to know if there's a way to validate domain credential and make sure we don't use the Cached Domain Credential ?
I use this to validate the credential :
bool valid = false;
using (PrincipalContext context = new PrincipalContext(ContextType.Domain))
{
valid = context.ValidateCredentials( username, password );
}
The problem is when I change the password, the old password is still working.
EDIT : If you force the password to be reset, the cached domain credential will not be use. But between the moment we force the reset, and moment the user reset the password, the old password will still work.
Question already has an answer Why does Active Directory validate last password?
Solution is to use a Kerberos authentication.
The following code shows how you can perform credential validation using only Kerberos. The authentication method at use will not fall back to NTLM in the event of failure.
private const int ERROR_LOGON_FAILURE = 0x31;
private bool ValidateCredentials(string username, string password, string domain)
{
NetworkCredential credentials
= new NetworkCredential(username, password, domain);
LdapDirectoryIdentifier id = new LdapDirectoryIdentifier(domain);
using(LdapConnection connection = new LdapConnection(id, credentials, AuthType.Kerberos))
{
connection.SessionOptions.Sealing = true;
connection.SessionOptions.Signing = true;
try
{
connection.Bind();
}
catch (LdapException lEx)
{
if (ERROR_LOGON_FAILURE == lEx.ErrorCode)
{
return false;
}
throw;
}
return true;
}
you might try something like this
try
{
using (var directoryEntry = new DirectoryEntry(ldapPath, userName, password))
{
var invocation = directoryEntry.NativeObject;
return true;
}
}
catch (Exception ex)
{
return false;
}
I want to create an Application for editing user accounts on a Server.
The Server do not use AD only local accounts.
I use the following code to connect the remote server:
try
{
PrincipalContext oPrincipalContext = new PrincipalContext(ContextType.Machine, "192.168.123.110", null, ContextOptions.Negotiate, "Administrator", "password");
try
{
MessageBox.Show(oPrincipalContext.ConnectedServer);
GroupPrincipal oGroupPrincipal = GroupPrincipal.FindByIdentity(oPrincipalContext, "Goetter");
try
{
// perform operations here
}
finally
{
oGroupPrincipal.Dispose();
}
}
finally
{
oPrincipalContext.Dispose();
}
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
}
Whenever I try this, I get an exception, that the user and or password is not authorized, independent of the user I use. Administrator is the build in Admin user account.
Does PrincipalContext only works with AD or also with local accounts? Is anything wrong with my code?
using(PrincipalContext oPrincipalContext = new PrincipalContext(ContextType.Machine, computer.Name, null, ContextOptions.Negotiate, Settings.UserName, Settings.UserPassword))
using(GroupPrincipal oGroupPrincipal = GroupPrincipal.FindByIdentity(oPrincipalContext, Settings.AdministratorsGroup))
{
// perform operations here
}
Change your code and wrap it around a using statement otherwise you may have some errors when trying to call the Dispose() method reason being when you try to dispose the connection may have already been closed by then.
you can use this code here and try either of the examples if you are using ActiveDirectory
example 1
If you work on .NET 3.5, you can use the System.DirectoryServices.AccountManagement namespace and easily verify your credentials:
// create a "principal context" - e.g. your domain (could be machine, too)
using(PrincipalContext pc = new PrincipalContext(ContextType.Domain, "YOURDOMAIN"))
{
// validate the credentials
bool isValid = pc.ValidateCredentials("myuser", "mypassword");
}
Example 2
using System.Security;
using System.DirectoryServices.AccountManagement;
public struct Credentials
{
public string Username;
public string Password;
}
public class Domain_Authentication
{
public Credentials Credentials;
public string Domain;
public Domain_Authentication(string Username, string Password, string SDomain)
{
Credentials.Username = Username;
Credentials.Password = Password;
Domain = SDomain;
}
public bool IsValid()
{
using (PrincipalContext pc = new PrincipalContext(ContextType.Domain, Domain))
{
// validate the credentials
return pc.ValidateCredentials(Credentials.Username, Credentials.Password);
}
}
}
The public bool IsValid() Method above should work for what you are looking for.
Have a look at PrincipalContext.ValidateCredentials
for your FindByIdentity portion you can try the following replacement code
string strName = System.Security.Principal.WindowsIdentity.GetCurrent().Name;
// This is here because of a .Net error that gets 0x80005000 on "isUser = user.IsMemberOf(groupU);"
string domainName = strName.Split('\\')[0];
var pc = new PrincipalContext(ContextType.Domain, domainName);
Additional Reference Link StackOverFlow Post
ContextType.Machine
i need to verify if the password is correct for a user.
i have this code:
private bool checkOldPasswordValid(string password, string username)
{
using (DirectoryEntry entry = new DirectoryEntry("WinNT://" + Environment.MachineName + ",computer"))
{
entry.Username = username;
entry.Password = password;
DirectorySearcher searcher = new DirectorySearcher(entry);
searcher.Filter = "(objectclass=user)";
try
{
searcher.FindOne();
}
catch (Exception ex)
{
return false;
}
return true;
}
}
but then directory searcher is not supported with WinNt, so i found another way to loop through all records.
foreach (DirectoryEntry dc in entry.Children)
{
// prints the name
System.Diagnostics.Debug.WriteLine(dc.Name);
}
but this just gets the name and doesnt verify the password.
please help . thanks
To autenticate against LDAP or WinNT, you need no DirectorySearcher. You only need to get the NativeObject from your DirectoryEntry instance. Here's a code sample that might guide you through the way.
public bool Authenticate(string username, string password, string domain) {
bool authenticated = false;
using (DirectoryEntry entry = new DirectoryEntry(#"WinNT://" + domain, username, password) {
try {
object nativeObject = entry.NativeObject;
authenticated = true;
} catch (DirectoryServicesCOMException ex) {
}
}
return authenticated;
}
This code will return either a user is authentic or not. Once you can get the NativeObject property using this DirectoryEntry class instance, this means that the AD (or local computer) used impersonation to get this object. If you get the object without having a thrown exception, this means that the AD (or local computer) was able to authenticate the impersonnated user.
While you can use the currently authenticated user by specifying no username and password, but only the domain (or local computer), by specifying a username and password, you say you want to use impersonnation, so the security infrastructure will use the given username and password to try to retrieve the NativeObject property from this DirectoryEntry class instance.
To authenticate against the AD, just replace the "WinNT://" for "LDAP://".
You can use DirectoryEntry itself.
See the example here: http://support.microsoft.com/kb/316748
Why are you using WinNT:// anyways?