Directory Authentication for Cross Domain Users in .NET





5.00/5 (3 votes)
Directory authentication for cross domain users in .NET
Introduction / Background
In web applications, we often come across situations where the user needs to be authenticated against Active Directory. .NET Framework has provided namespace System.DirectoryServices
which serves a purpose of communication between your application and Active Directory. The problem here is, when we try to authenticate a user which is not in the current domain in which domain our Impersonating user is there, then there are some specific challenges which we will be discussing here.
Abstract / Business Case
This paper talks about various techniques of authenticating a user over Active Directory, such as the PrincipalContext
class in .NET 3.5, DirectoryEntry
and LDAPConnection
class in .NET 2.0. These techniques are required by .NET applications in order to authenticate a user over Active directory. We will also be looking at the cross domain authentication problems and resolution to it in each framework release.
What is Cross Domain Authentication?
Say for example, we have the below scenario:
Our main Windows or a Web application is launched using a user which is in say X domain.
This application has an entry point where the credentials are asked and these credentials will be validated against active directory. The credentials will be in the form of UserName@DomainName.com. And the user is authenticated against corresponding domain.
While logging on, we provided the user credentials of Y domain and tried logging in. Now our application goes to the LDAP of domain Y and tries to authenticate the credentials of given user, this is called cross domain authentication.
In this case, as the user which is launching an application is from domain X and does not have access to the LDAP of domain Y, the DirectoryEntry
object will throw an error saying the user is not valid.
This problem mainly arises in web applications where the client users can be from any domain and want to access a particular application which is impersonated by a user of some different domain.
Problem Statement / Introduction
The problem comes when our website or a Windows application is using .NET Framework 2.0 and it is not possible in the near future to upgrade to the latest versions and wants to do a cross domain directory authentication. Please have a look at the below code:
public bool IsAuthenticated(string domain, string username, string pwd)
{
string domainAndUsername = domain + @"\" + username;
DirectoryEntry entry = new DirectoryEntry(_path, domainAndUsername, pwd);
try
{
//Bind to the native AdsObject for authentication.
object obj = entry.NativeObject;
DirectorySearcher search = new DirectorySearcher(entry);
search.Filter = "(SAMAccountName=" + username + ")";
search.PropertiesToLoad.Add("cn");
SearchResult result = search.FindOne();
if(null == result)
{
return false;
}
//Update the new path to the user in the directory.
_path = result.Path;
_filterAttribute = (string)result.Properties["cn"][0];
}
catch (Exception ex)
{
throw new Exception("Error authenticating user. " + ex.Message);
}
return true;
}
In this code, we have created an object of DirectoryEntry
, initialized it with a path and then authenticated using NativeObject
property of the AdsObject
. Here the _path
variable is nothing but the LDAP path which is formulated as shown below:
LDAP://CN=server,…,…,DC=domain,DC=com
In LDAP path, the server represents the machine on which the LDAP resides and subdomain and domain values represent the domain of that machine. The whole path can be treated as Fully Qualified domain Name of the LDAP server.
In usual scenarios, everything works fine when there is only one domain involved, in this case the impersonating user of an application will be in the same domain. So when the call to obj.NativeObject
goes, the native Ads
object is fetched from LDAP with the current user credentials. But when the user credentials are from the different domain, then the native object fails as it tries to verify the current user with different LDAP, it doesn’t get details of that user there and an exception is thrown.
Proposed Solution(s)
In case when we are using .NET Framework 3.5, the solution is simpler where the framework provided PrincipalContext
class does the work for us to authenticate user in any domain, below section details this solution. In case of .NET Framework 2.0, the situation is little tricky because we don’t have direct provision in DirectoryServices
namespace to authenticate the user in a specified domain. Below section details how this situation should be tackled.
Application of Solution
1. Solution using PrincipalContext
From .NET Framework 3.5 onwards, a new class is introduced to deal with Active Directory, we can find details about this on MSDN site. This class has a broader coverage but we will be looking at the Active Directory and LDAP interaction here. Below is a code which illustrates the solution:
private string AuthenticateUsingPrincipalcontext
( string strDomain, string strUserName, string strPassword)
{
string strDistinguishedName = string.Empty;
PrincipalContext ctx = new PrincipalContext(ContextType.Domain, strDomain);
try
{
bool bValid = ctx.ValidateCredentials(strUserName, strPassword);
// Additional check to search user in directory.
if (bValid)
{
UserPrincipal prUsr = new UserPrincipal(ctx);
prUsr.SamAccountName = strUserName;
PrincipalSearcher srchUser = new PrincipalSearcher(prUsr);
UserPrincipal foundUsr = srchUser.FindOne() as UserPrincipal;
if (foundUsr != null)
{
strDistinguishedName = foundUsr.DistinguishedName;
}
else
throw new AuthenticationException
("Please enter valid UserName/Password.");
}
else
throw new AuthenticationException
("Please enter valid UserName/Password.");
}catch(Exception ex)
{
throw new AuthenticationException
("Authentication Error in PrincipalContext.
Message: " + ex.Message);
}
finally
{
ctx.Dispose();
}
return strDistinguishedName;
}
In the above code, we can see the method ValidateCredentials
in Principalcontext
, this takes user name and password to validate the user on directory, before this, we have to create an object of Principalcontext
by providing the parameter as domain name and mentioning the context as Domain. Additional check is applied in order to verify the name of user by searching it in directory using DirectorySearcher
and fetching the distinguished name.
2. Solution using DirectoryEntry and LDAPConnection
This is very often that we cannot upgrade our long going big project to the latest version of .NET Framework due to project limitations and we have to work with the tools that are available with us in order to resolve the problem. The problem of cross domain authentication is one of this kind where we don’t have direct provision to authenticate user while the user from a different domain is running an application. In such a scenario, if the DirectoryEntry
is failing to authenticate the user, then this user should be verified using LDAPConnection
class. This class verifies the user against LDAP with its bare minimum details. The below code illustrates the solution:
private string AuthenticateUsingDirectoryEntry
(string strDomain, string strUserName, string strPassword)
{
string strDistinguishedName = string.Empty;
try{
// Temporarily created the path,
// there are various ways by which we can get the path.
string strPath = "LDAP://";
string[] domainArr = strDomain.Split('.');
foreach (string strDC in domainArr)
{
strPath += string.Format("DC={0},", strDC);
}
if (strPath.EndsWith(","))
strPath = strPath.Substring(0, strPath.Length - 1);
DirectoryEntry dirEntry = new DirectoryEntry(strPath);
// Additional facility, this should not be required anyway.
// We can Fetch default naming context of current user domain by below code.
if (dirEntry == null)
{
string strDN = string.Empty;
dirEntry = new DirectoryEntry("LDAP://RootDSE");
using (dirEntry)
{
strDN = dirEntry.Properties["defaultNamingContext"][0].ToString();
dirEntry.Dispose();
}
dirEntry = new DirectoryEntry("LDAP://" + strDN);
}
if (dirEntry == null)
throw new AuthenticationException("DirectoryEntry object cannot be
instantiated.");
DirectoryEntry userEntry = new DirectoryEntry
(dirEntry.Path, strUserName, strPassword);
try{
// This verifies the user with Active Directory and
// if user is not valid then exception is thrown.
object obj = userEntry.NativeObject;
}catch(Exception ex)
{
// Given user/password not valid.
// This happens if the impersonating user
// is in the different domain of the provide user
// We need an LDAP authentication in this case.
string strLink = "WinNT://" + strDomain.ToLower() + "/Domain Controllers";
DirectoryEntry _tempEntry = new DirectoryEntry(strLink);
object DomainControllers = _tempEntry.Invoke("members", null);
foreach (object dc in (System.Collections.IEnumerable) DomainControllers)
{
DirectoryEntry dcEntry = new DirectoryEntry(dc);
string strDNS = dcEntry.Name;
if (strDNS.EndsWith("$"))
strDNS = strDNS.Substring(0, strDNS.Length - 1);
try {
// If Bind fails, the user might not be available or the password
is in valid.
LdapConnection ldapConn = new LdapConnection(strDNS);
ldapConn.Credential = new
System.Net.NetworkCredential(strUserName, strPassword);
ldapConn.Bind();
break;
// TODO: might not be correct. Was : Exit For
}
catch (Exception exp)
{
//Failed on LDAP as well. User/Pwd not valid.
throw new AuthenticationException("Can't find user in LDAP." +
exp.Message);
}
}
}
// We are here it means, either our directoryEntry or LDAP has authenticated
the user.
// Do additional check to search the DistinguishedName
DirectorySearcher search = new DirectorySearcher(dirEntry);
search.Filter = "(SAMAccountName=" + strUserName + ")";
search.PropertiesToLoad.Add("distinguishedname");
search.ReferralChasing = ReferralChasingOption.All;
SearchResult result = search.FindOne();
if (result == null)
throw new Exception("Can't find user in LDAP");
strDistinguishedName = result.Properties["distinguishedname"][0].ToString();
}
catch (Exception ex)
{
throw new AuthenticationException("Authentication Error in DirectoryEntry.
Message: " + ex.Message);
}
finally
{
}
return strDistinguishedName;
}
}
As mentioned above, the LDAP path in a specific format is required by DirectoryEntry
and LDAPConnection
class in order to connect to active directory. The above code prepares this format and initializes the object of DirectoryEntry
. When we call obj.NativeObject
, the code actually goes and connects to directory and tries to formulate the native AdsObject
. Current impersonating user is used in order to connect to the LDAP, and if this user is not available in the active directory then an exception is thrown. This is a problem, where _path
contains the information about different domain and the impersonating user is in the different domain. To resolve this, we can use similar information and a LDAPConnection
class in order to connect to the LDAP of that different domain and authenticate the given user, in this situation, the impersonating user does not come in the picture as the credentials provided to LDAPConnection
object are used to connect to LDAP.
LDAPConnection
class required DNS to connect to LDAP, and as we don’t have that information, we again need to use a trick in order to first fetch the list of domain controllers and use one of the domain controller in that list for that domain. In order to fetch the list of domain controllers, the DirectoryEntry
class is used which will fetch the information using impersonating user and then the information related to domain controllers is used to formulate the LDAPConnection
object.
Additional verification using DirectorySearcher
is applied here as well which fetches the distinguished name of that user which we can verify further.
Limitation
The only limitation here is that the impersonating user needs to have access to both the domains and it should be able to fetch information from any domain.
Results / Conclusion
In order to make the correct authentication of user in .NET Framework 2.0, we can use a dual level check on DirectoryEntry
and LDAPConnection
class and also to search the user using its distinguished name in directory.