• Active Directory Fun

Jun
9

Active Directory

If you have spent any amount of time working w/ authentication in C#, I am sure that you have run across having to authenticate / query Active Directory.  Although it is really not a hard process, it can be a bit of a hassle in that there are a few gotchas and a few concepts that are not in your standard bag of tricks.   Below are a few tips on how to work w/ Active Directory and how to interface with it.  I am going to be focusing on how to work with users only.  Working w/ other objects (Computers, Groups, etc.) work in a similar fashion, but can get a bit more hairy that simply dealing w/ users.

  First off, you need to understand how you interface with Active Directory.  All connections, searches, and queries to Active Directory is done using LDAP.  Although LDAP can be a bit confusing  at first, you will find that it is very simple to master and use.  In fact, once you get pretty good with it, you can do advanced LDAP queries in Active Directory using the Active Directory Users and Computers tool in the Administrator Tools of windows.  (TIP:  if you have having trouble configuring a LDAP query, go to your Active Directory Users and Computers tool and run the query there.  You get instant results and it is much easier to test them. )

Nice Place to start w/ LDAP

Once you get the concept of LDAP under your belt, you are good to go w/ C#.  The biggest thing that you do need to do is to add a reference to System.DirectoryServices.  You can do this by adding a reference by right-clicking your project and choosing “Add Reference”.

One final hurtle you may run across before you can start pounding out the code is specifying your domain path in LDAP.  This not hard, but sometimes it throws people for a loop in figuring out how to structure it.  If I am querying users, I would use something like this…. “CN” being the container name I am trying to access…

LDAP://CN=Users,DC=nathanblevins,DC=com

From there on out, it is pretty much downhill.  Here are a few examples of things that you can do (that I use all the time…)

Note:  Most of these code samples are an excerpt from a larger class.  You will need to make sure the _DomainPath and _outputPath strings exist before this will compile.

Authenticating Users – This is a quick method used to authenticate users.  Pretty much, you need to have the username, password, and domain path in which to look for the user.  Also, I have added a quick method used to write the authentication attempts to a text file.  Only do this for debugging, please. 

 

/// <summary>
/// Accepts a username and password and tries to authenticate it in active directory.
/// </summary>
/// <param name="Password">The user’s password attempt.</param>
/// <param name="Username">The username you are trying to authenticate</param>
public bool AuthenticateUser(string Username, string Password)
{
    //Create a new directory entry object.
    DirectoryEntry entry = new DirectoryEntry(_DomainPath, Username, Password);

    //Write the attempt to a log file.
    if (_outputPath != String.Empty)
    {
        WriteAttemptToLogFile("Login Attempt: " + Username + " — " + Password + " — " + DateTime.Now.ToString());
    }

    try
    {
        DirectorySearcher search = new DirectorySearcher(entry);
        search.Filter = "(SAMAccountName=" + Username + ")";
        search.PropertiesToLoad.Add("cn");
        SearchResult result = search.FindOne();

        if (result == null)
        {
            //User Authentication Failed.
            return false;
        }
        else
        {
            //User Authentication Succeeded
            return true;
        }
    }
    catch
    {
        //Error thrown, user authentication failed.
        //TODO:  Add output varibles to specify the error.
        return false;
    }
}

/// <summary>
/// Writes a provided string to the specified log file.
/// </summary>
/// <param name="LogString">String that you wish to write to the long file.</param>
private void WriteAttemptToLogFile(string LogString)
{
FileInfo fi = new FileInfo(_outputPath);
if (fi.Exists)
{
    File.AppendAllText(_outputPath, LogString + "\n\r");
}
else
{
    File.WriteAllText(_outputPath, LogString + "\n\r");
}
}

 

Check for user existence by SamAccountName

/// <summary>
/// Checks to see if a Username exists in Active Directory.  No Password required.
/// </summary>
/// <param name="Username">The username that you want to check for</param>
public bool CheckToSeeIfUserExists(string Username)
{

//Start a new directory searcher that looks for a match in the SAMAccoutName records.
DirectorySearcher search = new DirectorySearcher();
search.Filter = String.Format("(SAMAccountName={0})", Username);
search.PropertiesToLoad.Add("cn");
SearchResult result = search.FindOne();

if (result == null)
{
    //No records found
    return false;
}
else
{
    //Record found, the username exists
    return true;
}
}

Sometimes you just need to know what is available to you.  In this case, I usually use this class that I have thrown together.  It has reference to most of the common Active Directory User properties and a brief description of what they are.  Usually, I cut what I really want out of this class, but feel free to use it in its entirety as well (although it is a bit messy).

public class ActiveDirectoryUser
{

private string _strCN;
/// <summary>
/// CN=Guy Thomas.  Actually, this LDAP attribute is made up from givenName joined to SN.
/// </summary>
public string StrCN
{
    get { return _strCN; }
    set { _strCN = value; }
}
private string _strDescription;
/// <summary>
/// What you see in Active Directory Users and Computers.  Not to be confused with displayName on the Users property sheet.
/// </summary>
public string StrDescription
{
    get { return _strDescription; }
    set { _strDescription = value; }
}
private string _strDN;
/// <summary>
/// DN is simply the most important LDAP attribute.
/// </summary>
public string StrDN
{
    get { return _strDN; }
    set { _strDN = value; }
}
private string _strGivenName;
/// <summary>
/// Firstname also called Christian name
/// </summary>
public string StrGivenName
{
    get { return _strGivenName; }
    set { _strGivenName = value; }
}
private string _strSAMAccountName;
/// <summary>
/// sAMAccountName = guyt.  Old NT 4.0 logon name, must be unique in the domain.  Can be confused with CN.
/// </summary>
public string StrSAMAccountName
{
    get { return _strSAMAccountName; }
    set { _strSAMAccountName = value; }
}
private string _strUserAccountControl;
/// <summary>
/// Used to disable an account.  A value of 514 disables the account, while 512 makes the account ready for logon.
/// </summary>
public string StrUserAccountControl
{
    get { return _strUserAccountControl; }
    set { _strUserAccountControl = value; }
}
private string _strUserPrincipleName;
/// <summary>
/// userPrincipalName = guyt@CP.com    Often abbreviated to UPN, and looks like an email address.  Very useful for logging on especially in a large Forest.   Note UPN must be unique in the forest.
/// </summary>
public string StrUserPrincipleName
{
    get { return _strUserPrincipleName; }
    set { _strUserPrincipleName = value; }
}
private string _strMail;
/// <summary>
/// An easy, but important attribute.  A simple SMTP address is all that is required billyn@ourdom.com
/// </summary>
public string StrMail
{
    get { return _strMail; }
    set { _strMail = value; }
}
private string _strHomeMDB;
/// <summary>
/// Here is where you set the MailStore
/// </summary>
public string StrHomeMDB
{
    get { return _strHomeMDB; }
    set { _strHomeMDB = value; }
}
private string _strMSExchHomeServerName;
/// <summary>
/// Exchange needs to know which server to deliver the mail.  Example:
/// </summary>
public string StrMSExchHomeServerName
{
    get { return _strMSExchHomeServerName; }
    set { _strMSExchHomeServerName = value; }
}
private string _strCompany;
/// <summary>
/// Company or organization name
/// </summary>
public string StrCompany
{
    get { return _strCompany; }
    set { _strCompany = value; }
}
private string _strDepartment;
/// <summary>
/// Useful category to fill in and use for filtering
/// </summary>
public string StrDepartment
{
    get { return _strDepartment; }
    set { _strDepartment = value; }
}
private string _strHomePhone;
/// <summary>
/// Home Phone number, (Lots more phone LDAPs)
/// </summary>
public string StrHomePhone
{
    get { return _strHomePhone; }
    set { _strHomePhone = value; }
}
private string _strPostalCode;
/// <summary>
/// Zip or post code
/// </summary>
public string StrPostalCode
{
    get { return _strPostalCode; }
    set { _strPostalCode = value; }
}
private string _strStreetAddress;
/// <summary>
/// First line of address
/// </summary>
public string StrStreetAddress
{
    get { return _strStreetAddress; }
    set { _strStreetAddress = value; }
}
private string _strTelephoneNumber;
/// <summary>
/// Office Phone
/// </summary>
public string StrTelephoneNumber
{
    get { return _strTelephoneNumber; }
    set { _strTelephoneNumber = value; }
}
private string _strSN;
/// <summary>
/// SN = Thomas. This would be referred to as last name or surname.
/// </summary>
public string StrSN
{
    get { return _strSN; }
    set { _strSN = value; }
}
private string _strState;
/// <summary>
/// State, Province or County
/// </summary>
public string StrState
{
    get { return _strState; }
    set { _strState = value; }
}
private string _strHomeDrive;
/// <summary>
/// Home Folder : connect
/// </summary>
public string StrHomeDrive
{
    get { return _strHomeDrive; }
    set { _strHomeDrive = value; }
}
private string _strOffice;
/// <summary>
/// Office! on the user’s General property sheet
/// </summary>
public string StrOffice
{
    get { return _strOffice; }
    set { _strOffice = value; }
}
private string _strLegacyExchangeDN;
/// <summary>
/// Legacy distinguished name for creating Contacts. In the following example,
/// </summary>
public string StrLegacyExchangeDN
{
    get { return _strLegacyExchangeDN; }
    set { _strLegacyExchangeDN = value; }
}
private string _strProxyAddresses;
/// <summary>
/// As the name ‘proxy’ suggests, it is possible for one recipient to have more than one email address.  Note the plural spelling of proxyAddresses.
/// </summary>
public string StrProxyAddresses
{
    get { return _strProxyAddresses; }
    set { _strProxyAddresses = value; }
}
private string _strManager;
/// <summary>
/// Boss, manager
/// </summary>
public string StrManager
{
    get { return _strManager; }
    set { _strManager = value; }
}
private string _strMobile;
/// <summary>
/// Mobile Phone number
/// </summary>
public string StrMobile
{
    get { return _strMobile; }
    set { _strMobile = value; }
}
private string _strDisplayName;
/// <summary>
/// displayName = Guy Thomas.  If you script this property, be sure you understand which field you are configuring.  DisplayName can be confused with CN or description.
/// </summary>
public string StrDisplayName
{
    get { return _strDisplayName; }
    set { _strDisplayName = value; }
}
private string _strCountryRegion;
/// <summary>
///Country or Region.
/// </summary>
public string StrCountryRegion
{
    get { return _strCountryRegion; }
    set { _strCountryRegion = value; }
}

/// <summary>
/// String of active directory property names.  Order matches the order of the property listings…
/// </summary>
private string[] ActiveDirectoryPropertyValues = new string[] {
    "cn",
    "company",
    "c",
    "department",
    "description",
    "displayName",
    "DN",
    "givenName",
    "homeDrive",
    "homeMDB",
    "homephone",
    "legacyExchangeDN",
    "mail",
    "manager",
    "mobile",
    "msExchHomeServerName",
    "physicalDeliveryOfficeName",
    "postalCode",
    "proxyAddresses",
    "sAMAccountName",
    "SN",
    "st",
    "streetAddress",
    "teleponeNumber",
    "userAccountControl",
    "userPrincipleName",
};
/// <summary>
/// Accepts a username to load user data.
/// </summary>
/// <param name="Username">Sam Account Name to Load.</param>
public ActiveDirectoryUser(string Username)
{
    DirectorySearcher search = new DirectorySearcher();
    search.Filter = String.Format("(SAMAccountName={0})", Username);

    for (int x = 0; x < ActiveDirectoryPropertyValues.Length; x++)
    {
        search.PropertiesToLoad.Add(ActiveDirectoryPropertyValues[x]);
    }

    SearchResult result = search.FindOne();

    //Take care to make sure order matches with that of the string[]ActiveDirectoryPropertyValues.
    _strCN = Convert.ToBoolean(result.Properties[ActiveDirectoryPropertyValues[0]].Count > 0) ? result.Properties[ActiveDirectoryPropertyValues[0]][0].ToString() : "";
    _strCompany = Convert.ToBoolean(result.Properties[ActiveDirectoryPropertyValues[1]].Count > 0) ? result.Properties[ActiveDirectoryPropertyValues[1]][0].ToString() : "";
    _strCountryRegion = Convert.ToBoolean(result.Properties[ActiveDirectoryPropertyValues[2]].Count > 0) ? result.Properties[ActiveDirectoryPropertyValues[2]][0].ToString() : "";
    _strDepartment = Convert.ToBoolean(result.Properties[ActiveDirectoryPropertyValues[3]].Count > 0) ? result.Properties[ActiveDirectoryPropertyValues[3]][0].ToString() : "";
    _strDescription = Convert.ToBoolean(result.Properties[ActiveDirectoryPropertyValues[4]].Count > 0) ? result.Properties[ActiveDirectoryPropertyValues[4]][0].ToString() : "";
    _strDisplayName = Convert.ToBoolean(result.Properties[ActiveDirectoryPropertyValues[5]].Count > 0) ? result.Properties[ActiveDirectoryPropertyValues[5]][0].ToString() : "";
    _strDN = Convert.ToBoolean(result.Properties[ActiveDirectoryPropertyValues[6]].Count > 0) ? result.Properties[ActiveDirectoryPropertyValues[6]][0].ToString() : "";
    _strGivenName = Convert.ToBoolean(result.Properties[ActiveDirectoryPropertyValues[7]].Count > 0) ? result.Properties[ActiveDirectoryPropertyValues[7]][0].ToString() : "";
    _strHomeDrive = Convert.ToBoolean(result.Properties[ActiveDirectoryPropertyValues[8]].Count > 0) ? result.Properties[ActiveDirectoryPropertyValues[8]][0].ToString() : "";
    _strHomeMDB = Convert.ToBoolean(result.Properties[ActiveDirectoryPropertyValues[9]].Count > 0) ? result.Properties[ActiveDirectoryPropertyValues[9]][0].ToString() : "";
    _strHomePhone = Convert.ToBoolean(result.Properties[ActiveDirectoryPropertyValues[10]].Count > 0) ? result.Properties[ActiveDirectoryPropertyValues[10]][0].ToString() : "";
    _strLegacyExchangeDN = Convert.ToBoolean(result.Properties[ActiveDirectoryPropertyValues[11]].Count > 0) ? result.Properties[ActiveDirectoryPropertyValues[11]][0].ToString() : "";
    _strMail = Convert.ToBoolean(result.Properties[ActiveDirectoryPropertyValues[12]].Count > 0) ? result.Properties[ActiveDirectoryPropertyValues[12]][0].ToString() : "";
    _strManager = Convert.ToBoolean(result.Properties[ActiveDirectoryPropertyValues[13]].Count > 0) ? result.Properties[ActiveDirectoryPropertyValues[13]][0].ToString() : "";
    _strMobile = Convert.ToBoolean(result.Properties[ActiveDirectoryPropertyValues[14]].Count > 0) ? result.Properties[ActiveDirectoryPropertyValues[14]][0].ToString() : "";
    _strMSExchHomeServerName = Convert.ToBoolean(result.Properties[ActiveDirectoryPropertyValues[15]].Count > 0) ? result.Properties[ActiveDirectoryPropertyValues[15]][0].ToString() : "";
    _strOffice = Convert.ToBoolean(result.Properties[ActiveDirectoryPropertyValues[16]].Count > 0) ? result.Properties[ActiveDirectoryPropertyValues[16]][0].ToString() : "";
    _strPostalCode = Convert.ToBoolean(result.Properties[ActiveDirectoryPropertyValues[17]].Count > 0) ? result.Properties[ActiveDirectoryPropertyValues[17]][0].ToString() : "";
    _strProxyAddresses = Convert.ToBoolean(result.Properties[ActiveDirectoryPropertyValues[18]].Count > 0) ? result.Properties[ActiveDirectoryPropertyValues[18]][0].ToString() : "";
    _strSAMAccountName = Convert.ToBoolean(result.Properties[ActiveDirectoryPropertyValues[19]].Count > 0) ? result.Properties[ActiveDirectoryPropertyValues[19]][0].ToString() : "";
    _strSN = Convert.ToBoolean(result.Properties[ActiveDirectoryPropertyValues[20]].Count > 0) ? result.Properties[ActiveDirectoryPropertyValues[20]][0].ToString() : "";
    _strState = Convert.ToBoolean(result.Properties[ActiveDirectoryPropertyValues[21]].Count > 0) ? result.Properties[ActiveDirectoryPropertyValues[21]][0].ToString() : "";
    _strStreetAddress = Convert.ToBoolean(result.Properties[ActiveDirectoryPropertyValues[22]].Count > 0) ? result.Properties[ActiveDirectoryPropertyValues[22]][0].ToString() : "";
    _strTelephoneNumber = Convert.ToBoolean(result.Properties[ActiveDirectoryPropertyValues[23]].Count > 0) ? result.Properties[ActiveDirectoryPropertyValues[23]][0].ToString() : "";
    _strUserAccountControl = Convert.ToBoolean(result.Properties[ActiveDirectoryPropertyValues[24]].Count > 0) ? result.Properties[ActiveDirectoryPropertyValues[24]][0].ToString() : "";
    _strUserPrincipleName = Convert.ToBoolean(result.Properties[ActiveDirectoryPropertyValues[25]].Count > 0) ? result.Properties[ActiveDirectoryPropertyValues[25]][0].ToString() : "";
}
}
</code>

Finally, a big gotcha… when you are wanting to sort your queries, you can call the Search Option method on your Active Directory Searcher.  On a side note, people sometimes get Filter and Sort mixed up.  Sorting specifies  the order in which you receive your result.  Filtering actually weeds out unneeded results.  Anyway, one of the big problems is that the active directory sorter will only allow you to sort on items that are indexed in active directory. Sadly, the display name and username are not indexed.  Therefore, if you want to sort them (to populate a drop down list or something), I would recommend implementing the IComparable Interface, like this…

Quick note – By implementing the interface, you are changing the Default sort option on that object.  You can add additional sorts by creating a public static Comparison<T> method.  Then, by adding an additional parameter to the Sort Method (changing it from the default sort), you can access those as well.  Pretty sweet!

<code type="c#">
public class ActiveDirectoryUserListing : IComparable<ActiveDirectoryUserListing>
{
private string _strDisplayName;

public string StrDisplayName
{
    get { return _strDisplayName; }
    set { _strDisplayName = value; }
}
private string _strUsername;

public string StrUsername
{
    get { return _strUsername; }
    set { _strUsername = value; }
}

public ActiveDirectoryUserListing(string Username, string Displayname)
{
    _strDisplayName = Displayname;
    _strUsername = Username;
}

#region IComparable<ActiveDirectoryUserListing> Members

public int CompareTo(ActiveDirectoryUserListing other)
{
    return StrDisplayName.CompareTo(other.StrDisplayName);
}

public static Comparison<ActiveDirectoryUserListing> UsernameComparison =
    delegate(ActiveDirectoryUserListing listing1, ActiveDirectoryUserListing listing2)
    {
        return listing1.StrUsername.CompareTo(listing2.StrUsername);
    };

#endregion
}

/// <summary>
/// Gets all the usernames in active directory and stuffs them into a List.
/// </summary>
public List<ActiveDirectoryUserListing> GetAllUsernames
{
get
{
    //Make the list.
    List<ActiveDirectoryUserListing> tempList = new List<ActiveDirectoryUserListing>();
    //Make the searcher object
    DirectorySearcher search = new DirectorySearcher();
    //Add the search filter… one must be present in order to get any results…
    search.Filter = "(&(objectCategory=user)(objectClass=person)(sAMAccountName=*)(!msExchUserAccountControl=2)(!Description=Built*)(!Description=Hide*))";
    search.PropertiesToLoad.Add("sAMAccountName");
    search.PropertiesToLoad.Add("displayName");

    SearchResultCollection result = search.FindAll();

    for (int x = 0; x < result.Count; x++)
    {
        //Make sure the items found has a username property (does not return null).  If it has a value, stuff it!
        string SAMAccountName = Convert.ToBoolean(result[x].Properties["sAMAccountName"].Count > 0) ? result[x].Properties["sAMAccountName"][0].ToString() : "";
        string DisplayName = Convert.ToBoolean(result[x].Properties["displayName"].Count > 0) ? result[x].Properties["displayName"][0].ToString() : "";

        if (SAMAccountName != String.Empty)
        {
            //Stuffed…
            tempList.Add(new ActiveDirectoryUserListing(SAMAccountName, DisplayName));
        }
    }

    return tempList;
}
}

Anyway, I hope this helps out a bit.  In the interest of space, I am not posting the entire class, but you have the bulk of the methods.   Hope this helps.