Click here to Skip to main content
15,881,757 members
Articles / Programming Languages / C#
Article

Directory Authentication for Cross Domain Users in .NET

Rate me:
Please Sign up or sign in to vote.
5.00/5 (3 votes)
18 Jun 2013CPOL6 min read 92.6K   1.5K   14   5
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:

C#
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:

C#
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:

C#
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.

References

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior)
India India
IUnknown.

Comments and Discussions

 
QuestionNot able to validate cross domain using above code Pin
jaideepsinh17-Apr-19 2:57
jaideepsinh17-Apr-19 2:57 
QuestionProblem with cross domain Pin
Member 965992813-Aug-14 1:15
Member 965992813-Aug-14 1:15 
AnswerRe: Problem with cross domain Pin
jaideepsinh17-Apr-19 3:35
jaideepsinh17-Apr-19 3:35 
QuestionCross Domain Authentication in .Net 3.5 + Pin
vbaddela2-May-14 10:04
vbaddela2-May-14 10:04 
How can cross domain authentication be performed in .Net 3.5 and above? (It is not obvious in the article)

I tried to use PrincipalContext with "Domain" type but I get the error messages "The server cannot be contacted", "The LDAP server is unavailable". THe "Machine" Context type also did not work.
AnswerRe: Cross Domain Authentication in .Net 3.5 + Pin
pravinsham21-Aug-14 21:00
pravinsham21-Aug-14 21:00 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.