In this section, we start diving into detail on the mechanics of binding to the directory using the DirectoryEntry object.
Binding Syntax
In ADSI and SDS, we use the term "binding" to describe the process of connecting to an object in an LDAP directory to read or modify its data. Use of the term "bind" is slightly different from the technical definition in LDAP, but we use this convention here. Please see the sidebar in Chapter 8, titled LDAP Bind vs. ADSI Bind, for more details.
To bind successfully to either Active Directory or ADAM, we need some key information:
Using the pseudocode representation in Listing 3.1 to demonstrate, this information corresponds exactly to one of the constructors available on DirectoryEntry.
Listing 3.1. Pseudocode Representation of Binding Syntax
DirectoryEntry entry = new DirectoryEntry( "{Provider}://{server:port}/{Hierarchy Path}", "{username}", "{password}", {Provider Options} ); |
Listing 3.2 shows a concrete example of what a typical Active Directory binding looks like.
Listing 3.2. Typical Active Directory Binding Demonstrating the Four Parameters
using System.DirectoryServices; //note the LDAP: is case-sensitive DirectoryEntry entry = new DirectoryEntry( "LDAP://DC=domain,DC=com", null, null, AuthenticationTypes.Secure ); |
In Listing 3.2, we are using the LDAP provider and no server name at all (called a serverless bind) to bind to the directory and access the domain root object. Since we did not supply a username or password (collectively referred to as credentials) and we specified AuthenticationTypes.Secure, the current Windows security context is used to access the directory automatically.
At this point, we have not actually bound to the directory. Because DirectoryEntry allows us to change the properties represented by the parameters on the constructor (such as Path and Username) after the object is constructed, it does not connect to the directory until it is forced. The first action, such as reading, or an action such as modifying the underlying directory object's properties, initiates the bind. This design gives us flexibility, but it can be confusing to people coming from an ADSI background, as it does not work this way in ADSI.
Any property or method that forces the connection will do. Typically, we use the RefreshCache method or the NativeObject property if we want to quickly force a bind:
//bind occurs at access here object nativeObj = entry.NativeObject; //or we could do this entry.RefreshCache();
ADSI Path Anatomy
Let's start drilling down on the various pieces that determine how DirectoryEntry binds to the directory, starting with Path. SDS uses the ADSI ADsPath syntax for specifying how objects in the directory are identified.
ADsPath is composed of these component parts:
:///
Table 3.1 shows a few concrete examples.
Example |
ADsPath |
---|---|
1 |
LDAP://mydc.mydomain.com/DC=mydc,DC=mydomain,DC=com |
2 |
GC://mydc/ |
3 |
LDAP://RootDSE |
4 |
LDAP://10.10.11.100:50000 |
As we can see, ADsPaths look a lot like URLs, and this is no coincidence. This string syntax is instantly familiar to most developers today, given the ubiquity of URLs.
Provider Syntax
In ADSI and SDS, the portion (which might be called the scheme if we were describing it in terms of URLs) determines the ADSI provider used to service the request. Because this book is about LDAP, we'll be using the LDAP provider in most cases. As we discussed in Chapter 1, Active Directory has the notion of a global catalog that contains a forest-wide, read-only copy of all of the objects in the forest (minus some of the attributes), so Active Directory supports a GC provider as well. The GC provider essentially just instructs ADSI to use the LDAP protocol, but to connect on port 3268 rather than port 389 (or on port 3269 rather than port 636, in the case of SSL/LDAP).
Warning: ADSI Providers Are Case Sensitive!
When specifying the provider in an ADsPath, do not use the spelling "ldap" or "Ldap," and so on, or it will not work and will result in the cryptic COM error message "0x80005000 Unknown error" when binding. The LDAP and GC providers must be specified in all capital letters.
Server
As we might imagine, the portion of ADsPath determines what server we wish to use. The server is extremely flexible in ADSI, especially when used with Active Directory. Active Directory supports
Example 3 (no server at all) bears special mention, and we actually cover that in the upcoming section called Serverless Binding to Active Directory.
With DNS-style names, Active Directory supports
If we do not specify a specific server name, either with serverless binding or by using the name of the domain, the Windows runtime will try to find an appropriate Active Directory server for us. Note that ADAM does not support this option, as ADAM is not a domain.
Serverless Binding to Active Directory
Serverless binding is available in Active Directory as a means to select any available domain controller in the local site to serve as the binding domain controller. By avoiding the specification of a particular server, we reduce the chances of overloading that server with requests, or of complete failure if that particular domain controller is down for maintenance or otherwise unavailable. A serverless bind will simply select the next available domain controller to bind with, first by site and then outside the site, thereby avoiding having to rely on a particular server to always be up and available. This leads to more scalable and robust applications.
In its simplest form, a serverless bind looks like this:
DirectoryEntry de = new DirectoryEntry("LDAP://DC=domain,DC=com");
Notice that we have an ADsPath that specifies an object name by its distinguished name (DN), but we have omitted the server.
So, how does it magically know what domain to use? It infers a domain to use based on the security context of the current thread. This will be either the account that the process was created with or an impersonated account. If the account is an Active Directory domain account (and not a local machine account), then the domain of that account is used to find a domain controller. The technology that enables serverless binding is called the Locator service and is covered in detail in Chapter 9.
Serverless binding works remarkably well in many scenarios when the current user's security context is a domain account. We use serverless binding liberally throughout the examples in this book, as the syntax is more compact and most of our samples are simple console applications, where it is probably reasonable to assume a domain account.
The main problem here is that serverless binding does not work at all under a local machine account. If used with a local machine account, a DirectoryEntry bind will generally yield a COMException of "0x8007203A The server is not operational". In this case, we must use one of the other valid server name syntaxes instead.
However, since so many examples (including most of ours) use serverless binding, it is often unclear that they are making this fundamental assumption about how the code will be executed.
Serverless binding tends to cause the most trouble in ASP.NET applications, where the security context can have so many variables and many of those involve the use of local machine accounts. See Chapter 8 for more details.
We also mentioned that Active Directory supports using the DNS name of the domain as valid server name syntax. An ADsPath with this syntax might look like this:
LDAP://domain.com/DC=domain,DC=com
This option is interesting, as it combines built-in failover capabilities offered by serverless binding without relying on the current security context to determine a domain to use in the first place. It is often a great choice to use in ASP.NET applications for the same reasons they are often problematic with serverless binding.
Recommendations for Server Name Syntax for Active Directory
Obviously, a lot of choices are available. However, we will attempt to simplify this with a single mandate:
Always use serverless binding or fully qualified DNS names!
SSL generally requires the use of the DNS name and Kerberos does not work well with IP addresses, often resulting in an NTLM authentication instead (which we would like to avoid if possible). We should not use NetBIOS and IP addresses, with the possible exception of test setups. Never use them in production deployments. Avoid unqualified DNS names as well.
We want to avoid using specific server names in Active Directory unless we are performing operations that require a specific server, such as synchronization.
Recommendations for Server Name Syntax for ADAM
For ADAM, we recommend using the fully qualified DNS domain name of the ADAM instance, if possible. The same reasons for using DNS names that apply to Active Directory apply to ADAM as well.
Recommendations for Specifying the Port
As with the server component in a web URL, the port part is optional unless we are using a nonstandard port. When using Active Directory, we never need to supply the port. Active Directory works only on the standard LDAP ports 389 and 636 for SSL. The global catalog is accessible only on ports 3268 and 3269 (again, for SSL). All we need to do is supply the correct provider (LDAP or GC) and set the appropriate binding flag to add SSL support, and ADSI will do the rest.
When using ADAM, it is more likely that it will be configured to listen for LDAP requests on different ports. The main reason is that multiple ADAM instances often coexist on the same machine. As such, it may become a requirement when using ADAM to supply the port information as well in the ADsPath.
Object Name Syntax in ADsPaths
The component specifies which object in the directory we wish to reference. In general, we will use the LDAP DN of the object for this. We discussed LDAP DNs in some detail in Chapter 1. Referring back to Table 3.1, examples 1 and 4 demonstrate using the DN. Example 1 shows a typical DN for a domain root-naming context in Active Directory, and example 4 shows a non-Active Directory DN, perhaps from ADAM. We can tell this is not an Active Directory DN because Active Directory does not use the O attribute (which stands for "organization") for object naming.
Example 2 shows an alternate object name syntax supported by both Active Directory and ADAM, which uses the object's GUID directly to specify the name. Active Directory and ADAM support a variety of special binding syntaxes that we explore in the upcoming sections on GUID, SID, and WKGUID binding.
The LDAP specification allows a directory to define provider-specific object naming syntaxes and to signify them by enclosing the name in <> characters. Active Directory and ADAM have done just that. Other directory platforms may define their own using this extension mechanism.
Finally, the third example shows the use of the RootDSE object. The object name RootDSE is a special "ADSI syntax" for referring to something called the LDAP V3 base DSA query. RootDSE is also covered in the section Binding to RootDSE, later in this chapter.
Special Character Considerations in Object Names
Largely, we do not have to worry about special characters in ADsPaths with LDAP. Most of the important rules apply to the DN in the part and the normal rules for DNs apply. However, there is one important exception to this rule. In a DN, the / character is legal, but in an ADsPath, this is a part separator, so it must be escaped with a character. Please refer back to the section LDAP Distinguished Names, in Chapter 1, for more details.
GUID Object Name Syntax
One of the special object name syntaxes supported by Active Directory and ADAM is the GUID style:
The angle brackets and the GUID keyword are required to inform the directory that an object should be referenced by its objectGUID attribute, rather than by its normal DN. The binding syntax itself is not that complicated; the treatment of guidvalue is what confounds most first-time ADSI and SDS users. Where does that value come from?
In ADSI, we could retrieve the GUID for any object by first searching for it, or binding to it, and then retrieving the objectGuid attribute, using the various GUID properties on DirectoryEntry or perhaps an extended DN search. It should be a simple matter of taking the returned GUID and binding to it. In .NET, there is even a managed Guid class to make string representations easier. However, there are some subtle issues to doing this correctly.
The first thing to understand is that Active Directory and ADAM support two syntaxes for guidvalue.
Thus, using this same GUID value, our GUID binding syntax could be either of these (neither is case sensitive for the value):
We can use whichever syntax we like, but we need to be careful not to get them mixed up or to build them incorrectly.
So, what exactly is the difference between these two formats? If we look closely, we can see that they contain the same data, but in a slightly different order. For the 16 bytes in the GUID (represented by the 32 hex characters), the COM GUID follows this pattern:
4 3 2 1 - 6 5 - 8 7 - 9 10 - 11 12 13 14 15 16
The first four bytes are in reverse order, then the second two, and then the third two. Bytes 916 are in the same order. Why is this?
The answer lies in the fact that the first eight bytes in a GUID structure are considered a 4-byte integer followed by two 2-byte integers. As you may know, integers in the Intel x86 world (which is historically almost synonymous with Windows) are stored in "little endian" order, which means that the least significant bytes come first. However, the COM GUID representation shows the values as hex-format integers in "reading" format, with the most significant bytes first, as we are used to seeing numbers printed and written.
Thus, the binary representation of the GUID is always different from the standard COM string syntax that we are used to seeing when we see GUIDs in print. Active Directory and ADAM store the GUID in the binary syntax, thus the NativeGuid property is shown in byte-order. The bottom line is that the Guid property, the NativeGuid property, and the objectGUID attribute in the directory all represent the exact same data.
Listing 3.3 demonstrates all of this.
Listing 3.3. Demonstration of Different GUID Binding Approaches
using System; using System.DirectoryServices; public static void GuidBindingTest() { DirectoryEntry rootDSE; DirectoryEntry domainRoot; DirectoryEntry entry1; DirectoryEntry entry2; DirectoryEntry entry3; string dnc; Guid objectGuid; Guid guidProperty; Guid convertedGuid; string nativeGuidString; string octetGuidProperty; using (rootDSE = GetEntry("rootDSE")) { dnc = (string) rootDSE.Properties["defaultNamingContext"].Value; } using (domainRoot = GetEntry(dnc)) { guidProperty = domainRoot.Guid; nativeGuidString = domainRoot.NativeGuid; objectGuid = new Guid((byte[]) domainRoot.Properties["objectGuid"].Value); } octetGuidProperty = BitConverter.ToString(guidProperty.ToByteArray()); octetGuidProperty = octetGuidProperty.Replace("-", "").ToLower(); byte[] nativeGuidBytes = new byte[16]; for (int i = 0; i<16; i++) { nativeGuidBytes[i] = Byte.Parse( nativeGuidString.Substring(i*2, 2), NumberStyles.HexNumber ); } convertedGuid = new Guid(nativeGuidBytes); Console.WriteLine("Guid property (COM syntax): {0}", guidProperty.ToString("D")); Console.WriteLine("NativeGuid (octet style): {0}", nativeGuidString); Console.WriteLine("Guid property (dashes removed): {0}", guidProperty.ToString("N")); Console.WriteLine("objectGuid (COM syntax): {0}", objectGuid.ToString("D")); Console.WriteLine("Guid Property as octet string: {0}", octetGuidProperty); Console.WriteLine("NativeGuid converted to Guid: {0}", convertedGuid.ToString("D")); string comBindingSyntax = String.Format("", guidProperty.ToString("D")); string nativeBindingSyntax = String.Format("", nativeGuidString); string invalidBindingSyntax = String.Format("", guidProperty.ToString("N")); using (entry1 = GetEntry(comBindingSyntax)) { entry1.RefreshCache(); //force bind Console.WriteLine( "{0} worked as expected.", comBindingSyntax); } using (entry2 = GetEntry(nativeBindingSyntax)) { entry2.RefreshCache(); //force bind Console.WriteLine( "{0} worked as expected.", nativeBindingSyntax); } using (entry3 = GetEntry(invalidBindingSyntax)) { try { entry3.RefreshCache(); //force bind } //this should fail unless there just happens //to be another object with the other GUID. //This is extremely unlikely! catch (COMException ex) { Console.WriteLine( "{0} failed as expected.", invalidBindingSyntax); } } //OUT: // //Guid property (COM syntax): // f5433a51-e10a-419e-8472-b368cc2abf2c //NativeGuid (octet style): // 513a43f50ae19e418472b368cc2abf2c //Guid property (dashes removed): // f5433a51e10a419e8472b368cc2abf2c //objectGuid (COM syntax): // f5433a51-e10a-419e-8472-b368cc2abf2c //Guid Property as octet string: // 513a43f50ae19e418472b368cc2abf2c //NativeGuid converted to Guid: // f5433a51-e10a-419e-8472-b368cc2abf2c // // worked as expected. // // worked as expected. // // failed as expected. } private static DirectoryEntry GetEntry(string dn) { return new DirectoryEntry( "LDAP://" + dn, null, null, AuthenticationTypes.Secure ); } |
From this, we can see that the Guid and NativeGuid properties and the objectGUID attribute represent the same data in different formats, and that we can transform between all three easily.
We also see that using the Guid.ToString("N") method does not do what we want. It looks like it produces a GUID in octet string syntax (no dashes), but really it just uses the standard COM GUID string format with dashes removed. This is not what we want!
When creating GUID binding strings, just remember that if the string contains dashes, the GUID must be in COM string format, and if it does not contain dashes, it must be in binary octet string format.
Note: GUID DN Syntax Is an LDAP Feature
The GUID DN syntax is not a feature of ADSI, but it is supported by LDAP and the directory itself. This means that any API talking to Active Directory or ADAM can use this syntax in place of a DN, not just ADSI or SDS. It works perfectly fine in the Win32 LDAP or System.DirectoryServices.Protocols. However, it is an Active Directory and ADAM feature. Other directories may not support this syntax. This rule applies to all of the special binding syntaxes we are describing here.
Internally, objectGUID is essentially the primary key for objects in Active Directory and ADAM. Unlike the DN, it is rename-safe, even in cross-domain moves. All objects in the directory have an objectGUID attribute and it is impossible to change it without doing some dangerous hacking. As such, it makes an ideal primary key for storage in other systems. In SQL Server, for example, a column of type "unique identifier" is perfect for storing the objectGUID attribute. This creates a durable, immutable foreign key pointing to Active Directory or ADAM data for application scenarios that may require it, such as custom synchronization tasks. Since we are storing the directory object's GUID in the database, it makes perfect sense to use the GUID binding syntax to find the object in the directory.
Well-Known GUID Object Name Syntax
One of the problems that applications must contend with is that we often need to access certain "well-known" objects in Active Directory, such as the Domain Controllers container, but we may not always know their names from one install to the next. The names may vary with different internationalized installations of Windows.
Microsoft solves this particular naming issue the same way it solves many other similar problems in Windows. It specifies a well-known GUID that can be used to alias a particular object, regardless of its "normal" name. This well-known GUID will be the same in all deployments of Active Directory. In order to use the well-known GUID in a bind, there is another, special DN syntax:
This DN syntax is similar to the syntax, except there are two parts to it:
(GUID part) is the well-known GUID of the object and it must be specified in octet string syntax. This is unlike normal GUID binding syntax, where both octet string and COM GUID formats are allowed. (DN of the domain partition) is the actual DN of the domain partition, assuming we are looking for well-known objects in the domain partition. We need this, of course, because a well-known GUID is not unique (hence, it is well known!) and we still need to know which Active Directory domain we are referring to. There are also three well-known objects in the Configuration container that would use its DN here instead. These DNs will vary from directory to directory but can be determined dynamically from RootDSE.
Listing 3.4 demonstrates how to bind to the 'CN=Users' container in a domain.
Listing 3.4. Well-Known GUID Binding
string adsPath = "LDAP://"; using (DirectoryEntry de = new DirectoryEntry( adsPath, null, null, AuthenticationTypes.Secure )) { Console.WriteLine("Successfully Bound To: {0}", de.Properties["distinguishedName"].Value); } //OUT: Successfully Bound To: CN=Users,DC=domain,DC=com |
Table 3.2 shows the well-known GUIDs that are defined.
Note: The Well-Known GUID and objectGUID Are Not the Same
The well-known GUID is not the same as the target object's objectGUID attribute. The well-known GUID will be the same in all implementations, whereas the actual objectGUID will vary with each installation. They are used for two different purposes.
Container |
GUID Identifier |
---|---|
Users |
GUID_USERS_CONTAINER_W |
Computers |
GUID_COMPUTERS_CONTAINER_W |
Systems |
GUID_SYSTEMS_CONTAINER_W |
Domain Controllers |
GUID_DOMAIN_CONTROLLERS_CONTAINER_W |
Infrastructure |
GUID_INFRASTRUCTURE_CONTAINER_W |
Deleted Objects |
GUID_DELETED_OBJECTS_CONTAINER_W |
Lost and Found |
GUID_LOSTANDFOUND_CONTAINER_W |
The value for these GUID Identifier constants is defined in the ntdsapi.h header file in the Windows Platform SDK.
The well-known object mechanism is extensible via the otherWellKnownObjects attribute, so it is possible to add our own well-known objects to the directory if we wish.
SID Object Name Syntax
Similar to GUID binding, SID binding is an alternate way to locate an object in Active Directory or ADAM using its security identifier (SID). SIDs are the unique identifiers used by Windows for security principals. The SID used for binding in Active Directory and ADAM is stored in the objectSid attribute. However, unlike the objectGUID attribute, not every object in the directory has a SID. Only objects that have the securityPrincipal auxiliary class will have an objectSid attribute. Luckily, this includes many of the objects we are interested in, such as users, groups, and computers.
Similar to objectGUID, the objectSid value remains the same even if that object has been renamed or moved. A major difference between objectSid and objectGuid is that since a SID is tied to a domain, it cannot be moved across domains. Instead, if an object moves across domains, it will be assigned a new SID for the domain (the objectGUID will remain the same), and the previous SID will be added to the object's sidHistory attribute.
The SID DN style uses the following syntax:
Like the GUID syntax, sidvalue can be one of two different syntaxes:
Note: SDDL Format Works Only with Windows Server 2003 Active Directory and ADAM
We have to be careful which environment we are targeting when using a SID bind. Only the Windows Server 2003 version of Active Directory and ADAM supports the SDDL syntax, so we must use the octet string syntax with Windows 2000. If we have a mixed environment or are uncertain which platform will be used, it is better to err on the side of caution and use the octet string format. It is possible to determine the directory version at runtime by examining several different attributes on RootDSE, such as supportedCapabilities.
.NET 2.0 has a convenient new SecurityIdentifier class with a ToString method that will produce SDDL output given the binary form. However, for .NET 1.x, the SDDL format of the SID is not available without P/Invoking a Win32 API (ConvertSidToStringSid), or using the ADsSecurity.dll library. As such, it is often easier to bind with the SID using the octet string syntax if we are given the binary version to start with. When retrieving the SID from Active Directory with the objectSid attribute, the binary format is returned.
Listing 3.5 shows a handy function for generating octet strings from binary data such as GUIDs and SIDs. It is so handy, in fact, that we reference it several more times throughout the book!
Listing 3.5. BuildOctetString Function
using System.Text; private string BuildOctetString(byte[] bytes) { StringBuilder sb = new StringBuilder(); for(int i=0; i < bytes.Length; i++) { sb.Append(bytes[i].ToString("X2")); } return sb.ToString(); } |
Listing 3.6 demonstrates how we might use this function.
Listing 3.6. Demonstrating SID Binding
DirectoryEntry user = new DirectoryEntry( "LDAP://CN=User1,CN=Users,DC=domain,DC=com", null, null, AuthenticationTypes.Secure ); //retrieve the SID byte[] sidBytes = user.Properties["objectSid"].Value as byte[]; //format the bytes using the BuildOctetString function string adPath = String.Format( "LDAP://", BuildOctetString(sidBytes) ); DirectoryEntry sidBind = new DirectoryEntry( adPath, null, null, AuthenticationTypes.Secure ); //force the bind object native = sidBind.NativeObject; |
As with the earlier GUID binding sample, this example is somewhat contrived because we would not first bind to an object and then retrieve its SID, only to rebind again. A more realistic use for this is to examine the user object's tokenGroups attribute, which is a multivalued attribute that contains the SIDs of all of the security groups of which the user is a member. We could create a bindable SID string, use it to bind to any of the security groups, and then examine the group object. A sample in Chapter 10 demonstrates this.
Like GUIDs, SIDs can also be stored in external databases and used as keys into Active Directory data. They are not as convenient to use as GUIDs, as the binary data is of a variable length and not all objects have a SID to begin with. However, there are certain applications where this might be appropriate.
Warning: SID Binding Does Not Work with the Global Catalog
Unlike the other special binding syntaxes, SID binding is available only over the LDAP port on Active Directory. Attempting to do a SID bind against the global catalog will result in an error.
Providing Credentials
Throughout most of this book, we use samples like Listing 3.7 for binding to the directory.
Listing 3.7. Binding with Default Credentials
DirectoryEntry entry = new DirectoryEntry( "LDAP://DC=domain,DC=com", null, null, AuthenticationTypes.Secure ); |
Here, we are specifying null for the username and password parameters. This is also called supplying default credentials and it instructs Windows to use the current security context to access the directory.
However, we do not always wish to access Active Directory or ADAM with our currently logged-on credentials. We may need to bind using an ADAM account (which requires explicit credentials) or we may be running under a local machine account that cannot access the domain. To accommodate this, the DirectoryEntry supports accepting alternate credentials in plaintext, as shown in Listing 3.8.
Note: Using the AuthenticationTypes.Secure Flag
We are using the Secure option with the provider in order to protect the credentials we have specified and to keep them from being transmitted in plain text over the wire. Note also that the username and password we have specified are used to create the security context that will be used to bind to the directory. You can find more detail on secure binding later in this chapter and in Chapter 8. We generally use secure binding throughout the samples in this book, where appropriate, because secure binding is generally a good thing to do.
Listing 3.8. Binding with Explicit Credentials
DirectoryEntry entry = new DirectoryEntry( "LDAP://DC=domain,DC=com", @"domainadminuser", "password", AuthenticationTypes.Secure ); //equivalent syntax DirectoryEntry entry = new DirectoryEntry( "LDAP://DC=domain,DC=com" ); entry.Username = @"domainadminuser"; entry.Password = "password"; entry.AuthenticationType = AuthenticationTypes.Secure; |
Suppose we wanted to bind directly to a user object rather than to the root domain, as we did in Listing 3.8. Listing 3.9 demonstrates what this might look like.
Listing 3.9. Binding to a Specific User with Default Credentials
DirectoryEntry entry = new DirectoryEntry( "LDAP://CN=Joe Somebody,CN=Users,DC=domain,DC=com" ); entry.Username = @"domainadminuser"; entry.Password = "password"; entry.AuthenticationType = AuthenticationTypes.Secure; |
It can be confusing to first-time SDS users what is happening here, because the credentials we have specified are not those of Joe Somebody, the user to which we are binding! Don't we need to supply Joe's username (domainjsomebody) and know his password in order to bind correctly?
The answer is, of course, no. Remember, we said earlier that the credentials supplied were used to create the security context with which to bind to the directory. Those credentials do not represent the object to which we are binding. Instead, they are the credentials of whatever user we wish to use to perform the task we need to perform. This could be a very low-privileged account that can just read a few attributes on a few objects, an administrator with power over every object in the domain, or something in between.
Chapter 8 thoroughly explores the security aspects of binding and the implications of supplying and omitting credentials.
Username Syntaxes in Active Directory and ADAM
One of the most confusing aspects about supplying credentials is to know the right format with which to specify the username parameter. The LDAP specification states that the user's full DN must be accepted as a username when doing simple binds. However, it allows directories to accept additional username syntaxes as well. Active Directory supports four different username syntaxes that we can use:
However, we cannot use all of them interchangeably. Different flags, set via the AuthenticationTypes enumeration, control which username format we can use. Here are the rules governing use for each of them.
[1] DN syntax actually can be used with AuthenticationTypes.Secure, but only if the user's common name (CN) is the same as the sAMAccountName. ADSI will take the user's CN from the DN and use this as the username in a secure bind. Given that the CN does not necessarily equal the sAMAccountName, this technique is inherently unreliable and we recommend against using it. It is also not documented by Microsoft, so the behavior may change at any time.
Table 3.3 summarizes the allowed combinations.
Username Syntax |
AuthenticationTypes Requirement |
---|---|
NT Account Name |
Any |
User Principal Name |
Any |
Plain Account Name |
AuthenticationTypes.Secure |
Distinguished Name |
Not AuthenticationTypes.Secure |
Recommendation: Use the NT Account Name or UPN
Given that the NT Account Name and the UPN may always be used, it seems to make the most sense to use those consistently in our code.
The UPN is the new recommended username syntax in Windows 2000 and higher, and it should generally be favored over the NT Account Name. However, there is one potential gotcha with the UPN in that Active Directory does not enforce uniqueness on the userPrincipalName attribute, even though it is required for every UPN to be unique in the entire Active Directory forest. If two or more users share the same UPN, none of them will be able to authenticate using their UPN. These issues do not occur with the NT Account Name, as every account has a user name (sAMAccountName) that is guaranteed to be unique by the directory and every domain has a unique domain name.
There may be occasions where using the plain username is important. For example, we may not know the domain name or have the UPN. In that case, make sure to use AuthenticationTypes.Secure.
There may also be occasions where we want to bind with the DN. For example, the DN is the username binding syntax that is actually defined by the LDAP simple bind specification, so many applications that try to work across different LDAP servers may choose to use this as a lowest common denominator format. In this case, make sure you do not use AuthenticationTypes.Secure so that a simple bind will be used instead. Please see the section AuthenticationTypes Explained for precautions regarding using a simple bind.
Username Syntaxes in ADAM
ADAM is a little less complicated than Active Directory when authenticating ADAM users. By ADAM users, we mean user objects stored in ADAM, not in Active Directory (see Chapters 8 and 10 for more details). Two username formats may be used in ADAM:
The DN is required by the LDAP specification, as we previously explained. The UPN in ADAM is a little bit different from the UPN in Active Directory, though. ADAM has no concept of a forest and does not integrate with Kerberos, so the syntax requirements for UPN in ADAM are more relaxed in that it does not even need to contain an "@" sign. Essentially, we can put whatever value we wish in the ADAM UPN attribute, as long as it is unique. Note that the UPN is not a mandatory attribute in the ADAM user object schema, so it may not be set. If we wish to use it for binding, we must populate the value first.
In the case of ADAM users, we use an LDAP simple bind to authenticate them with SDS, so we must use AuthenticationTypes.None to accomplish this.
AuthenticationTypes Explained
The last component that determines the behavior of the DirectoryEntry bind was called "provider-specific options." In SDS terminology, we mean authentication types, which are represented by the AuthenticationTypes enumeration.
AuthenticationTypes specify options that change the mechanisms used by SDS to connect and communicate with the directory. It is vitally important to understand what each one does and how they work together, so we will now examine each member of the enumeration in detail.
Enumeration Members
Listing 3.10 shows the members of the AuthenticationTypes enumeration.
Listing 3.10. AuthenticationTypes Enumeration Members
[Flags] public enum AuthenticationTypes { Anonymous = 0x10, Delegation = 0x100, Encryption = 2, FastBind = 0x20, None = 0, ReadonlyServer = 4, Sealing = 0x80, Secure = 1, SecureSocketsLayer = 2, ServerBind = 0x200, Signing = 0x40 } |
We will explore each member in detail. Instead of proceeding in alphabetical order, we will group the values together in terms of their importance to each other.
None
This is the default value for the enumeration. None is most often used when an LDAP simple bind is desired (see Chapter 8 for more details on what simple bind really means). An LDAP simple bind is the only binding mechanism defined in the actual LDAP version 3 specification, so it has excellent compatibility across LDAP server vendors. Unfortunately, it relies on a plaintext exchange of credentials, so it is completely insecure by itself.
Warning: This Option Is Not Secure!
We repeat again, this option is not secure by itself. Please do not use None unless it is combined with SecureSocketsLayer or some other external means of transport security such as IPSEC. Do not pass plaintext credentials on an unencrypted network channel!
Note that this is not the default value for any of the DirectoryEntry constructors that do not specify AuthenticationTypes in .NET 2.0. .NET 2.0 uses Secure by default. Unfortunately, in .NET 1.x, the behavior varies depending on the constructor used. Chapter 8 contains an important caveat about the behavior differences this change can cause.
Secure
This option indicates to use the Windows Security Support Provider Interface (SSPI) authentication system when binding to the directory. In terms of SDS use, this is roughly synonymous with using the Windows Negotiate protocol.[2] Negotiate authentication selects between Kerberos and NTLM authentication through a negotiation process (hence the name). Negotiate is the native authentication protocol of Windows in Windows 2000 and later. Secure binding supports both explicit credentials and using the current Windows security context for authentication, which is incredibly useful in many situations. Chapter 8 discusses these options in detail.
[2] Windows Server 2003 can use Digest authentication when Secure authentication is selected, and it detects that the server supports Digest authentication but does not support Negotiate, as might be more likely on a non-Windows directory. This behavior may also make its way into other operating systems. However, Secure authentication almost always means Negotiate authentication for typical scenarios.
Sealing
This flag says to use the encryption capabilities of SSPI to encrypt traffic after the security context is established. Because it relies on SSPI, it must be combined with the Secure flag in order to work.
Not all SSPI authentication protocols support encryption, and they behave differently on different operating system versions. Kerberos authentication always supports encryption (although different operating systems support different encryption strengths). However, NTLM support for encryption was not originally available in Windows 2000 and instead made its appearance originally with Windows XP. To discover which operating systems support SSPI encryption on which protocol, it is best to check the most recent documentation, as this even tends to vary from one service pack to another and not just with full operating system releases.
Signing
Signing uses the signing capabilities of SSPI to sign network traffic and verify whether someone has tampered with it. As with Sealing, it must be combined with the Secure flag in order to work.
Signing and Sealing are very often used together. Signing also has the same caveats as Sealing in terms of support with various protocols on different operating system revisions.
Delegation
Delegation refers to the ability of a service to use an authenticated user's security context to access another service on the network. Delegation is a feature offered by the Kerberos authentication protocol. Like Sealing and Signing, it must again be combined with the Secure flag, or it has no effect.
A lot of confusion surrounds this particular value because it seems to indicate that somehow delegation will occur if it is used, or that it must be specified when using delegation. In fact, neither of these is true. It is entirely possible for delegation to be available if we do not explicitly request it, and it is possible to request it and not get it. However, if we want delegation, it is always best to use this flag to signal our intent. We discuss delegation scenarios in detail in Chapter 8, including how to detect whether delegation is available.
Anonymous
This option tells ADSI not to perform a Bind operation before attempting other operations, such as searches. As such, the state of the LDAP connection will not be authenticated.
In order to use Anonymous successfully, we must also supply username and password credentials with an empty string ("" or String.Empty). We cannot use null/Nothing. If we try to use a null reference for our credentials, we will receive a somewhat cryptic "invalid parameter" exception.
This flag is not typically used with Active Directory, as unauthenticated users can do very little in the directory. In fact, Windows Server 2003 Active Directory and ADAM do not allow anonymous operations by default. This flag is generally used with non-Microsoft directories that allow completely anonymous access.
SecureSocketsLayer
SecureSocketsLayer specifies that the SSL/ TLS protocol will be used to encrypt the network traffic with the directory server, including the Bind request. When specifying this option, an SSL certificate must be installed and available in Active Directory and ADAM.
Under the covers, ADSI will change the TCP port (if it is not already specified) from the default port 389 to port 636, and SSL will be used to secure the communication.
SSL is often supported by third-party LDAP directories and should be the preferred method of protecting credentials when communicating with directories other than Active Directory and ADAM. SecureSocketsLayer is also helpful in some situations when using the ADSI SetPassword and ChangePassword methods (see Chapter 10).
Note that we can combine Secure authentication with SecureSocketsLayer, but we cannot combine SecureSocketsLayer with Sealing or Signing.
Encryption
This enumeration value is actually the same as the SecureSocketsLayer value and they do exactly the same thing. However, Encryption is the deprecated name and should not be used.
ServerBind
Unlike many of the other flags described so far, this flag affects the DNS lookup behavior of Bind. ServerBind essentially says "I am supplying an exact server name in my ADsPath, so do not bother using DNS to try to dynamically discover a domain controller using the Locator service." It is a performance optimization only, as it eliminates the extra DNS traffic involved in the dynamic discovery process. It is never required.
Given this, we should never combine ServerBind with serverless binding or binding with the domain name.
ServerBind is most often used with ADAM and non-Microsoft directories. It can be used with Active Directory, but it eliminates Active Directory's automatic failover capabilities and can cause brittleness as a result. It is useful with scenarios when a specific domain controller is required, such as for synchronization or for specific FSMO operations.
We can combine ServerBind with any other flags.
FastBind
In order to provide some of the special ADSI interfaces for an object, such as IADsUser, that correspond to specific types of objects in the directory, ADSI attempts to determine the object's schema type when the object is first accessed. It does this by retrieving the object's objectClass attribute. Once the objectClass attribute is known, ADSI can map all of the extra ADSI interfaces to the object. This initial search is done in addition to the search operation used to fill the property cache.
In effect, ADSI does two searches every time it accesses an object and reads its properties. When accessing a large number of objects in batch operations (for instance, to update a particular attribute), all of these little search operations can add up to a huge performance hit.
FastBind disables this initial search to determine the objectClass attribute. This can cause remarkable performance increases. Unfortunately, it comes with a drawback. Only the following high-level ADSI interfaces will be available to the object:
One thing that is interesting about this list is that all of the operations performed by the DirectoryEntry class in normal usage use only these ADSI interfaces under the hood. We need the other interfaces that FastBind prevents only when using the various Invoke methods to access members of other interfaces, such as IADsUser and IADsGroup. As such, this flag can probably be used for an effective performance boost much more often than it is.
Additionally, this option does not verify that the object exists during binding, which can complicate error handling should this situation not be expected. For example, with FastBind, we cannot use the NativeObject property to verify an object's existence. We must perform an operation that loads the property cache, such as RefreshCache, as only then is an actual search performed.
We can combine FastBind with all other AuthenticationTypes.
ReadonlyServer
In Active Directory and ADAM as of Windows Server 2003, all servers are writable, so this flag has no effect. It is generally there for use with Windows NT4 and the WinNT provider where backup domain controllers exist.
However, the next version of Windows Server (currently code-named "Longhorn Server") will introduce something called the "read-only DC," so perhaps this flag will take on new meaning then.
Recommendations for AuthenticationTypes
So, which values should we use? Because AuthenticationTypes is a bitwise type of enumeration, we can combine the values, so there are many options. We cannot provide guidance on every single combination, but we can make some recommendations.
So, in a scenario where we are talking to Active Directory using a specific server and we need delegation, secure communications, and maximum performance, we would use this:
AuthenticationTypes.Secure | AuthenticationTypes.Sealing | AuthenticationTypes.Signing | AuthenticationTypes.Delegation | AuthenticationTypes.FastBind | AuthenticationTypes.ServerBind
Table 3.4 summarizes the rules for use of AuthenticationTypes.
Value |
Requires |
Often Combined With |
Do Not Combine With |
---|---|---|---|
Secure |
Anonymous |
||
Sealing |
Secure |
Signing, Delegation |
SecureSocketsLayer |
Signing |
Secure |
Sealing, Delegation |
SecureSocketsLayer |
Delegation |
Secure |
Signing, Sealing |
|
Anonymous |
Secure |
||
SecureSocketsLayer |
Signing, Sealing |
||
ServerBind |
Any |
||
FastBind |
Any |
||
ReadonlyServer |
Any |
Binding to RootDSE
One of the shortcomings of the original LDAP specification was that there wasn't an easy way to know information about a given directory at runtime. For example, it is nice to know the DNs of the naming contexts exposed by the directory, the schema it exposes, and the various capabilities it supports.
The primary LDAP version 3 specification, RFC 2251, addresses this by defining a well-known root DSE object (a DSA-specific entry, where DSA is a standard X.500 term for the directory server itself) available on all servers that provides specific information about the directory. The root DSE object is accessible by performing a base-level search against a null DN as the search root with a filter of (objectClass=*).
ADSI provides a shortcut for accessing the root DSE object by providing a special DN for it, called (predictably) RootDSE. Thus, an ADsPath for the root DSE object would look like this:
LDAP://RootDSE LDAP://mydc.mydomain.com/RootDSE
As with all DNs, RootDSE is not case sensitive. We can capitalize it any way we want. Because there is no actual object named "RootDSE" in the directory, we cannot perform a search looking for it. The RootDSE DN is just syntactic sugar understood by ADSI to give us a shortcut to find it. Thus, in a lower-level API such as System.DirectoryServices.Protocols (SDS.P), we would actually have to perform the base-level search on the null search root DN, as the term "RootDSE" is valid only in ADSI.
RootDSE Attribute Data
RFCs 2251 and 2252 define the following attributes that all LDAP version 3 servers must support.
Furthermore, Active Directory and ADAM support the following additional properties.
Uses for RootDSE
RootDSE is extremely useful for determining information about the server dynamically at runtime. One of the most common things we will do in LDAP programming is perform subtree searches on the default naming context of the server. Instead of having to know the DN of this object to use as a search base, we can use RootDSE to tell us this information. Listing 3.11 demonstrates this.
Listing 3.11. Retrieving the Default Naming Context with RootDSE
DirectoryEntry rootDSE = new DirectoryEntry("LDAP://RootDSE"); String dnc = (string) rootDSE.Properties["defaultNamingContext"].Value; DirectoryEntry searchRoot = new DirectoryEntry("LDAP://" + dnc); |
If you are at all like us, you will likely use code such as this in nearly every project. We use the default naming context for building the DN of not only the domain root, but also the known children in the domain. Given that these values rarely change, it is probably a good idea from a performance perspective to cache these values if they are being used in a larger application framework.
Another very helpful attribute is the dnsHostName attribute. If we did not specify a specific server name in our binding string, we may wish to know to which server we actually connected. This is one easy way to find out. All of the other attributes have varying degrees of usefulness depending on the application.
RootDSE is also useful if we need to authenticate a user's credentials via ADSI. Chapter 12 discusses LDAP authentication in detail.
Here are a few other useful things to keep in mind about RootDSE.
ADSI Connection Caching Explained
As we explained in Chapter 1, the LDAP API is based on the concept of operations performed on a persistent connection to the server. However, ADSI and SDS are based on a metaphor of reading and modifying objects in the directory. Since we know ADSI is using LDAP under the hood, we know that there must be an LDAP connection being used somewhere. So, how exactly does that work? Does ADSI create and destroy a new connection with each object access? Does it somehow reuse an existing connection that it maintains?
As you may have guessed by the title of this section, ADSI does in fact cache and reuse LDAP connection handles under the hood when it can. This is important because there is significant overhead in establishing a connection and binding to authenticate it. We would not want to do this repeatedly if we could avoid it. Furthermore, in high-volume server scenarios where many different connections are being opened and closed quickly, we run the risk of running out of TCP "wildcard ports," which could cause the underlying network layer to fail. We will not explain wildcard ports in detail here, but suffice it to say that if we are building a server application with high scalability requirements, we don't want to run out of them. This section is important!
Here is how it works. If a client in a particular process makes a connection to a server by creating and binding a DirectoryEntry object, the connection will be reused if all three of the following are true.
Listing 3.12 shows an example.
Listing 3.12. Demonstrating Proper Connection Caching
DirectoryEntry rootDSE = new DirectoryEntry( "LDAP://myserver.mydomain.com/RootDSE" ); //this will force the connection to be established Object bindTest = rootDSE.NativeObject; //server, port and credentials are the same. //connection will be reused, even though the object //"user" will be released when the using block exits DirectoryEntry user; string cn; using (user) { user = new DirectoryEntry( "LDAP://myserver.mydomain.com/" + "CN=myuser,CN=Users,DC=myserver,DC=mydomain,DC=com" ); cn = (string) user.Properties["cn"].Value; } DirectoryEntry newUser; string cn2; using (newUser) { //This will start a new connection because the //AuthenticationTypes are different newUser = new DirectoryEntry( "LDAP://myserver.mydomain.com/" + "CN=myuser2,CN=Users,DC=myserver,DC=mydomain,DC=com", null, null, AuthenticationTypes.SecureSocketsLayer ); string cn2 = (string) newUser.Properties["cn"].Value; } //The second connection is closed as we leave the using block //and the newUser DirectoryEntry is disposed //We are now cleaning up the original object. //The connection will be closed rootDSE.Dispose(); |
Let's review some of the finer points here. The first thing to remember is that the connection stays open as long as at least one object that uses the connection stays open. This means that at least one object must not be Disposed or finalized by the garbage collector. In order to prevent finalization, we have to keep a live root to the object in memory. That generally means maintaining the object in a long-lived object member, such as in a static variable in a class.
Another thing to remember is that connections are cached per process. This is especially important to remember in the context of ASP.NET, where a single process may host multiple AppDomains containing different web applications that are not related. If we reuse servers, ports, credentials, and AuthenticationTypes between different web applications in the same server process, the various web applications will reuse each other's cached connections.
Connection Caching with Serverless Binds
When we use serverless binding, the LDAP API will determine an Active Directory server for us automatically, using the Locator service. So then, if we repeatedly create DirectoryEntry objects in this way and we don't explicitly control the server being used, will connection caching still work?
The short answer is maybe. The Locator service will generally continue to return the same server, which will allow connection caching to work. It does this by caching the Locator call. However, after some time, the Locator service is contacted again to avoid stale data. It is also possible for other processes on the machine to force a cache refresh. The point here is that it is entirely possible for a serverless bind to return a different domain controller. We should keep in mind the fact that we do not have a guarantee that we will always get the same domain controller and avoid writing code that expects this behavior. If a new domain controller is returned because of a cache refresh, we will get a new connection.
The story with binding with default credentials is pretty much the same. As long as the security context stays the same, connection caching will continue to work. However, if the security context changes (due to a change in thread impersonation, for example), then a new connection will be created. This last point is important to remember, because if we are impersonating many different users in a high-volume application, a new connection will be opened for each one and we may have scalability issues as a result.
Directory CRUD Operations |
Part I: Fundamentals
Introduction to LDAP and Active Directory
Introduction to .NET Directory Services Programming
Binding and CRUD Operations with DirectoryEntry
Searching with the DirectorySearcher
Advanced LDAP Searches
Reading and Writing LDAP Attributes
Active Directory and ADAM Schema
Security in Directory Services Programming
Introduction to the ActiveDirectory Namespace
Part II: Practical Applications
User Management
Group Management
Authentication
Part III: Appendixes
Appendix A. Three Approaches to COM Interop with ADSI
Appendix B. LDAP Tools for Programmers
Appendix C. Troubleshooting and Help
Index