Binding controls the authenticated security context of the client when it performs LDAP operations on the server. Because the server typically performs authorization on every operation requested by the client, establishing an authenticated security context with the server is essential.
A lot of options are involved with binding, especially in Windows, so we devote a significant amount of space in this chapter to explaining the details. As we have suggested previously, binding and security issues are the most common source of problems in .NET directory services programming, so it is well worth the time to try to understand it fully.
In SDS, the DirectoryEntry object is responsible for performing all binding operations. The other core object, DirectorySearcher, relies completely on DirectoryEntry in its SearchRoot property to determine its security context. Even if we do not explicitly specify a SearchRoot, we should keep in mind that one will be created for us.
Types of Binds
In LDAP terms, there are essentially two types of binds: a simple bind and a bind that uses a Simple Authentication and Security Layer (SASL) mechanism to perform the authentication. A simple bind requires plaintext credentials and transmits them on the network in clear text. As such, they are certainly simple, but obviously not secure.
SASL is much more interesting. SASL is defined in RFC 2222 and specifies an extensible mechanism where various authentication protocols such as Kerberos, NTLM, and Digest can plug into the SASL structure, providing access to these authentication protocols via LDAP (and any other protocols that use SASL; it is a separate specification that is not part of LDAP).
A directory may support various SASL mechanisms, and it advertises which ones it supports via the RootDSE object we discussed in Chapter 3 in the supportedSASLMechanisms attribute. Active Directory supports the GSSAPI, GSS-SPNEGO, EXTERNAL, and DIGEST-MD5 SASL mechanisms.
GSS-SPNEGO is Microsoft's SASL implementation of the Windows Negotiate protocol, which is the standard authentication protocol for Windows 2000 and later. The Negotiate protocol is often misunderstood to mean that Kerberos will be attempted first and NTLM will be the fallback protocol. Actually, Negotiate means that the protocol will negotiate with the server which protocol will be used based on its capabilities, and only one is ever tried. This is an important distinction, because certain types of attacks are prevented that could be caused if an attacker could arbitrarily downgrade authentication to NTLM.
GSSAPI is a component that is typically provided with standard Kerberos implementations and is included with Microsoft's Kerberos for standards interoperability. Digest implements the MD5 Message-Digest algorithm (RFC 1321). EXTERNAL is how Microsoft implements client certificate authentication with SSL/LDAP.
From the ADSI and SDS perspectives, we really are interested in two types of binds: the simple bind and the SASL bind with GSS-SPNEGO. These are reflected via the AuthenticationTypes.None and AuthenticationTypes.Secure enumeration values. ADSI and SDS do not provide access to the other mechanisms.[1] We will refer to GSS-SPNEGO SASL binds as "secure" binds from now on.
[1] This is not completely true, as ADSI does support client certificate authentication with SSL, and ADSI in Windows Server 2003 will attempt Digest authentication if AuthenticationTypes.Secure is specified and the server advertises that it supports Digest authentication but not the Negotiate protocol. However, we do not have explicit control over these options and they are rare cases that affect very few scenarios.
Where simple binds always require a username and password to be specified, secure binds can accept credentials or use the credential information from the current Windows security context. This additional feature is extremely powerful and enables a new class of available scenarios for binding. However, this feature also provides no end of trouble when it is not understood fully, due to some of the underlying complexity of Windows security itself.
Performing a Secure Bind
As we previously stated, secure binds allow for us to bind to the directory with either the current Windows security context or explicitly provided credentials. To perform a secure bind in SDS, we thus have two options with the DirectoryEntry constructors.
Using default credentials might look like this:
DirectoryEntry entry = new DirectoryEntry( "LDAP://DC=mydomain,DC=com");
We can also write code that does the exact same thing, like this:
DirectoryEntry entry = new DirectoryEntry( "LDAP://DC=mydomain,DC=com", null, null);
To supply explicit credentials, we can write code like this:
DirectoryEntry entry = new DirectoryEntry( "LDAP://DC=mydomain,DC=com", "mydomainsomeone", "MyBig!Secret88", AuthenticationTypes.Secure);
Note that we can also use the constructor that just takes the username and password parameters and rely on the fact that it sets the default value for AuthenticationTypes to AuthenticationTypes.Secure. However, we generally prefer being explicit to avoid confusion.
It is also possible to combine default credentials with the AuthenticationTypes.Secure flag, as we often do in our code examples:
DirectoryEntry entry = new DirectoryEntry( "LDAP://DC=mydomain,DC=com", null, null, AuthenticationTypes.Secure);
We again tend to prefer this syntax, as it is more explicit and makes the intent of our code easier to decipher. We are not relying on any default behaviors that might possibly change or be misunderstood by our successors. See the warning titled Behavior Change in .NET 2.0! for an example of how we can already get into trouble.
The important thing to note here is that if we supply default credentials, a secure bind is always attempted, regardless of our AuthenticationTypes. This seems like a little unintuitive at first and is one of the reasons we always try to be explicit in our code. Even though we tend to use the AuthenticationTypes.None flag when we intend to do a simple bind, if we also supply null for the username and password parameters, we will not get a simple bind. We will get a secure bind.
This makes sense when we think about it, as the AuthenticationTypes.None value is equal to zero. It isn't a real value at all for bitwise operations! When this flag is combined with other flags, it does not change the overall value. For example:
AuthenticationTypes.None | AuthenticationTypes.Secure = AuthenticationTypes.Secure
As such, AuthenticationTypes.None is useful when supplying specific credentials, but it makes no sense to use it when we are not. When it is combined with other flags, it does not change the overall value. We tend to use it this way only to help convey our intent that we want a simple bind.
Being able to use the current security context is very powerful in that we do not need to hardcode any credentials, or prompt the user who is executing the code to supply them. Unfortunately, this feature gets us into trouble in many cases, as it makes certain assumptions about the current security context that may or may not be true. The rest of this section is dedicated to exploring these issues.
Windows Security Contexts
It is important to have a reasonable understanding of what we mean by Windows security contexts before moving any further. This subject is reasonably large in and of itself and a complete treatment is not possible, so we refer to Keith Brown's book,[2] The .NET Developer's Guide to Windows Security, for the full story.
[2] Brown, K. 2004. The .NET Developer's Guide to Windows Security. Boston: Addison-Wesley.
Each thread executing code inside a Windows NT-based process (Windows NT all the way through 2000, XP, 2003, Vista, and beyond) will have a security context represented by a Windows security token that governs the rights and privileges that will be granted to code executing on that thread. In general, the token that will be used by the thread for security purposes is the token that was used to start the underlying process. This is determined by whom and how the process was originally created. Processes launched by an interactive user will use the token associated with that user's log-on session. Processes such as Windows services will use the token associated with whatever the service was configured to use in the Service Control Manager, and so on.
Warning: Behavior Change in .NET 2.0!
What happens if we do not specify any AuthenticationTypes in our constructor? Obviously, the DirectoryEntry must have some sort of default behavior, but what? Let's take a simple example:
//What is the difference between this... DirectoryEntry de = new DirectoryEntry( "LDAP://mydomain.com/RootDSE", user, password); //...and this? DirectoryEntry de2 = new DirectoryEntry( "LDAP://mydomain.com/RootDSE"); de2.Username = user; de2.Password = password;
In .NET 1.x, AuthenticationTypes on the DirectoryEntry was set to None for some constructors and AuthenticationTypes.Secure for others. As a result, the two DirectoryEntry objects in the preceding code will produce different binds. The first one will produce a secure bind, but the second one will result in a simple bind with credentials passed in plaintext over the network.
Since this behavior is inherently insecure, Microsoft changed in version 2.0 and now always uses AuthenticationTypes.Secure as the default if the developer did not specify anything. The result is that the .NET 2.0 DirectoryEntry is secure by default, which is a good thing. However, since this behavior is different, it can lead to unexpected breaking changes when migrating 1.x code to 2.0 if the code was counting on a simple bind. This is most likely to cause problems with non-Active Directory directories that do not support Microsoft's GSSSPNEGO SASL mechanism (all of them, to our knowledge), but it can also cause problems with ADAM where simple and secure binds are used to authenticate two different types of users. It could also break code where firewall rules prevent secure binds from working (e.g., only the LDAP port is open to the domain controller).
The lesson here is one that we have tried to demonstrate consistently throughout this book: Be explicit! If we always specify the AuthenticationTypes we want to use, we will never be surprised by default behavior and will have a much better idea of how and why our code works.
There is more to the story here, though, because Windows also has a feature known as impersonation. Impersonation allows an application to change the security token of a specific thread to a token other than the process token. When a thread is in this state, it is said to be impersonating. Impersonation is often used in server processes where it is useful to execute code on behalf of the various users accessing the system, instead of with the security context of the process itself. The server process can impersonate the user using her credentials and can access local resources with her security context. IIS, with or without ASP.NET, is one such system and will likely be the application that readers of this book are most familiar with and interested in.
In summary, a thread executing code will always have a security context represented by a Windows security token. This token will be either the token that was used to launch the process, or the token that the server created via impersonation by using the client's credentials.
Windows security tokens are very interesting because they can be generated by various mechanisms, such as interactive logins with usernames and passwords, using smart cards, or the exchange of Kerberos tickets. In addition, several different APIs in Windows are used to create and manage tokens.
IIS actually supports several of these mechanisms for generating security tokens for users. It can accept a user's plaintext credentials via Basic authentication and can perform a local login to authenticate the user and create a token. Digest authentication allows IIS to authenticate a user and create a token without exchanging plaintext credentials. IIS can also use Integrated Windows Authentication to create the user's security context via either Kerberos or NTLM (the Negotiate protocol again), without prompting the user. When SSL is used, IIS can use client certificates to create a security token for the user.
Now, let's take this back to SDS. As we stated previously, when using secure binding, we can either specify explicit credentials or use the current security context by specifying null for the Username and Password on the DirectoryEntry. An example of the latter that we have already used extensively throughout this book might be as follows:
DirectoryEntry entry = new DirectoryEntry( "LDAP://rootDSE", null, null, AuthenticationTypes.Secure );
Here, since we have specified the Secure binding flag and have not specified credentials, the current security context will be used. As we now understand, this security context will be the identity of the current thread, which will be either the process identity or an impersonated identity. The token could have been established in any of the ways that we previously described.
Figure 8.1 demonstrates how the interaction with Active Directory works under the hood during a typical secure bind.
Figure 8.1. Active Directory Bind Using Secure Authentication
Tip: How Do I Discover the Current Security Context?
All versions of the .NET Framework offer an easy way to discover the identity of the current security context, regardless of whether impersonation is in effect. The static method System.Security.Principal.WindowsIdentity.GetCurrent() will return a WindowsIdentity object that essentially wraps the security token. The Name property on WindowsIdentity will tell us the Windows username associated with it.
Single Hops, Double Hops, and Delegation
Up to this point, we have been discussing security contexts and tokens and how our server applications can impersonate them in order to access local resources on our behalf. What happens when we want our server to use our impersonated context to access a remote resource? In this case, we have to use a feature called delegation. Delegation is a security-sensitive operation that is not enabled by default. We will set the basics here and refer again to Keith Brown's security book for the really gritty details.
Prior to Windows 2000, delegation was not supported because of the way NTLM authentication worked. Essentially, NTLM authentication relied on the client to encrypt a unique challenge hash using a key derived from the user's password. This encrypted challenge was called the response. The server would subsequently forward the same challenge and the client's response to a domain controller, where it would be validated that it could have come only from our client. The server itself never had access to the client's password nor any key material derived from it. Now, if the server attempted to access a remote resource on the client's behalf, it would in turn be challenged. With no way to encrypt the challenge hash using the client's password (since the server never had it), delegation would fail at that point.
NTLM still works this way today and cannot be used for delegation. Instead, we must use a protocol that supports delegation. For our purposes, we are talking about Kerberos today. The Kerberos protocol supports delegation via the forwarding of a client's Kerberos ticket. This allows a server to use the client's credentials for a limited time to access any remote resources by default. We can constrain what resources our server can access with a client's credentials in Windows 2003 by using a feature called constrained delegation. We will speak more about this feature later. Figure 8.2 illustrates how Kerberos delegation works at a high level. The figure uses a few terms to describe some of these scenarios, so let's define them now.
Figure 8.2. Kerberos Delegation
Single Hop
A single hop occurs when a user sends his security credentials to a single remote network resource. In SDS, this would be equivalent to running a console application on the local machine that accesses the directory on a remote machine and uses Secure binding with default credentials.
Double Hop
A double hop occurs when a user sends his security credentials to a remote network service and then that server attempts to forward the user's credentials to a remote resource. In SDS, this occurs frequently in web applications that use Integrated Windows Authentication in IIS. The user's security context on the client machine running the web browser is used to authenticate with IIS. The web application code attempts to use impersonation along with default credentials binding to access the directory on a remote server.
The key with the double hop is that it does not work by default. As we previously stated, delegation is not enabled for an Active Directory account by default. Windows does not allow just any service to forward a user's credentials to another network server resource. In our example, the security context presented by the IIS machine to the directory server will not be accepted and the user will be authenticated as the anonymous user. Since this special account rarely has rights to do anything on the directory, most types of operations will fail. As the IIS scenario we described is quite common, failures due to double-hop issues are some of the most common issues that .NET directory services programmers encounter.
Delegation
Delegation is the feature in Windows that allows a service to access another network service on the user's behalf. When Microsoft changed the native Windows authentication protocol to use Kerberos with Windows 2000, it was able to take advantage of features of the Kerberos protocol to make delegation possible. Because it is a security-sensitive operation, the administrator of the domain must allow the account running the service process to use delegation by marking the account as "trusted for delegation" from the Active Directory Users and Computers (ADUC) MMC. Once this is done, that account can then delegate most other accounts' credentials to other services on the network.
With Kerberos, we do this with tickets. Essentially, once we have set an account as trusted for delegation, any tickets issued for the trusted service to our clients will be marked as "OK to delegate" and they can then be forwarded to remote services. However, this leaves us in a predicament with administrative accounts. Essentially, we have a "trusted" account that can use the credentials of any other account (including domain administrators). A rogue application using an account trusted for delegation could wait for or lure an administrator to use the application and then easily elevate their own privileges or attack other systems. As such, delegation can also be blocked on specific user accounts by marking them as "sensitive and cannot be delegated" from the ADUC MMC. This is often done with highly privileged accounts such as those of domain administrators and account operators, but it can be applied to any account in Active Directory. This prevents the scenario we just discussed, as an administrator's account marked as sensitive cannot be used by an account trusted for delegation.
Constrained Delegation
In Windows 2000, delegation was an all or nothing proposition. If an account was configured for delegation, it could delegate to any and all resources. In Windows Server 2003, a new feature called constrained delegation has been added. Constrained delegation is just like it sounds; an account may delegate, but only to specific services. This makes delegation much more appealing to administrators, as they can be very specific about what services an account may access when it is delegating another user's credentials.
Discovering Remote Security Information at Runtime
Often it is useful to know details about the remote security context on the Active Directory server that the bind operation creates. For example, we may wish to know whether we successfully authenticated with Kerberos, as that allows us to determine proactively whether delegation will be possible.
In .NET 2.0, SDS introduces a new class called DirectoryEntryConfiguration that provides a mechanism to do just this. The IsMutuallyAuthenticated method returns a Boolean based on whether mutual authentication (meaning Kerberos for us today) was used for the bind. An example might look like this:
DirectoryEntry entry = new DirectoryEntry( "LDAP://DC=mydomain,DC=com", null, null, AuthenticationTypes.Secure ); Console.WriteLine(entry.Options.IsMutuallyAuthenticated());
In this example, the result would generally be true if this was run from a console application and Kerberos authentication was working correctly. However, if we changed AuthenticationTypes to None and added credentials, the result would be false. Note that this method will throw an exception if the bind fails.
Determining mutual authentication status is useful, but the underlying interface that ADSI uses, IADsObjectOptions, actually provides detailed information about the remote security context in the form of the return flags from the InitializeSecurityContext Windows API function. Also, since .NET 1.1 does not support the new class, it is useful to have our own wrapper that we can use. Listing 8.1 shows an example of one such wrapper.
Listing 8.1. Retrieving Raw SSPI Result Flags
[Flags()] public enum IscRetFlags { ISC_RET_DELEGATE = 0x00000001, ISC_RET_MUTUAL_AUTH = 0x00000002, ISC_RET_REPLAY_DETECT = 0x00000004, ISC_RET_SEQUENCE_DETECT = 0x00000008, ISC_RET_CONFIDENTIALITY = 0x00000010, ISC_RET_USE_SESSION_KEY = 0x00000020, ISC_RET_USED_COLLECTED_CREDS = 0x00000040, ISC_RET_USED_SUPPLIED_CREDS = 0x00000080, ISC_RET_ALLOCATED_MEMORY = 0x00000100, ISC_RET_USED_DCE_STYLE = 0x00000200, ISC_RET_DATAGRAM = 0x00000400, ISC_RET_CONNECTION = 0x00000800, ISC_RET_INTERMEDIATE_RETURN = 0x00001000, ISC_RET_CALL_LEVEL = 0x00002000, ISC_RET_EXTENDED_ERROR = 0x00004000, ISC_RET_STREAM = 0x00008000, ISC_RET_INTEGRITY = 0x00010000, ISC_RET_IDENTIFY = 0x00020000, ISC_RET_NULL_SESSION = 0x00040000, ISC_RET_MANUAL_CRED_VALIDATION = 0x00080000, ISC_RET_RESERVED1 = 0x00100000, ISC_RET_FRAGMENT_ONLY = 0x00200000 } public class AuthenticationStatusChecker { private enum AdsOption { ADS_OPTION_ACCUMULATIVE_MODIFICATION = 8, ADS_OPTION_MUTUAL_AUTH_STATUS = 4, ADS_OPTION_PAGE_SIZE = 2, ADS_OPTION_PASSWORD_METHOD = 7, ADS_OPTION_PASSWORD_PORTNUMBER = 6, ADS_OPTION_QUOTA = 5, ADS_OPTION_REFERRALS = 1, ADS_OPTION_SECURITY_MASK = 3, ADS_OPTION_SERVERNAME = 0 } public static bool IsMutuallyAuthenticated(DirectoryEntry entry) { IscRetFlags authStatus = GetAuthStatus(entry); if ((IscRetFlags.ISC_RET_MUTUAL_AUTH & authStatus) == IscRetFlags.ISC_RET_MUTUAL_AUTH) { return true; } else { return false; } } public static IscRetFlags GetAuthStatus(DirectoryEntry entry) { object val = entry.Invoke("GetOption", new object[] {AdsOption.ADS_OPTION_MUTUAL_AUTH_STATUS}); return (IscRetFlags) val; } } |
One of the benefits of this class is that if we have a problem with delegation and we end up with a null authentication session on the directory server as a result, GetAuthStatus will return a value containing the ISC_RET_NULL_SESSION flag. This can be a very handy troubleshooting technique.
Guidance for Using SDS with ASP.NET
ASP.NET offers practically every option available to build web applications on the Windows platform, including the full suite of Windows security features and a variety of ASP.NET security features as well as forms authentication and role-based security. Deciding how to build our application may seem overwhelming at first, but it is not difficult if we make a few important decisions up front.
Trusted Subsystem versus Delegated Model
In a trusted subsystem architecture, we use a specific service account to access a remote resource instead of impersonating the user who is accessing the system. We configure the service account with the appropriate rights to access whatever resources we are interested in. Users of the application are authorized to the resources by the application itself.
The delegated model is essentially the opposite. The remote resource is accessed using the security context of the authenticated user, not a service account. The application must be able to delegate the user's credentials and act on the user's behalf when accessing the remote resource.
An example of a trusted subsystem scenario might be an outsourced helpdesk application. In this common scenario, we have many changing and unknown helpdesk users providing services such as password resets and group creation to a company. As long as our application can authorize the users as valid helpdesk personnel, we will not have to grant rights to any of these users' accounts directly. Our service account will hold all the rights to reset passwords and create groups, and the application will enforce that only helpdesk personnel can execute these tasks. In this high-turnover type of industry, we will not want to grant these rights directly to the helpdesk users, because the high turnover forces us to be constantly maintaining security on the accounts. Additionally, these privileges could be abused outside the scope of the application if given directly to the users.
An example of a delegated model might be an online user directory where users may update certain parts of their profile (e.g., telephone number, name, etc.). By default, Active Directory allows users to update certain attributes on their own user object. If we provide them with an interface with which to access their own user object, we can delegate their credentials and allow them to make the changes themselves. In this manner, we do not have to worry about a user accessing someone else's user account and making changes. We know that if we use delegation, the only thing the user can accomplish is based on the rights that the user has in the directory (of which updating another user's account is not one).
Both approaches are valid and will be appropriate in different contexts. Some applications make better candidates for one approach versus another. In very large applications with many users, it is often easier to administer a single service account with permission to perform particular actions than it is to try to modify the underlying user's account to give this permission to each user. This is a good candidate for a trusted subsystem approach. In general, if some permission is required for our users in an application that we would not want them to have all the time under all circumstances, we should be using a trusted subsystem model.
Applications where it is imperative that we know and allow our users to perform only what they are allowed to perform are good candidates for delegation models. Administrative tools and web sites are good examples where we have relatively few users that should have the permissions necessary to perform whatever action our application attempts.
Type of Authentication in the Web Application
This is important for any type of web application, but it a much more important decision in a delegated model because we must choose a form of authentication that allows delegation! Essentially two approaches work here.
[3] Windows Server 2003 adds a new feature called Protocol Transition or Kerberos Service for User (S4U) that actually allows a server to authenticate a user with one Windows authentication protocol, such as Digest or NTLM, and transition to Kerberos in order to delegate to a remote resource using the new constrained delegation feature.
Both approaches allow us to delegate the user's security context to the directory. Note that with any type of authentication scheme that collects the user's credentials in plaintext, appropriate encryption such as HTTPS/ SSL must be used to make the application secure!
Example Approaches for a Trusted Subsystem
Now that we understand what a trusted subsystem is, what are the options for building one in ASP.NET that works with SDS? The first thing to consider is that we need to create a service account in the directory whose credentials will be used. We should give the service account the permissions it needs to perform the tasks the application requires (and no more). Once we have that, there are really two basic approaches here.
Let's take each of these in turn. Configuring the process identity depends on the version of IIS we are using.
Windows Server 2003
With IIS 6 on Windows Server 2003, each application pool may be configured with a specific identity directly. We simply set our application pool identity to our given service account with the proper permissions our application needs. We can then in turn configure our IIS application to use a specific application pool. Since ASP.NET runs in process with our application pool, this means our .NET code will execute with the configured service account's security context. This is by far the easiest of our options to use. We don't have to worry about storing or exposing credentials, and this solution works like a charm. Note that when we are using the process identity as our service account, we must ensure that impersonation is disabled in the web.config file or that we disable impersonation programmatically while performing our SDS tasks.
Windows 2000 Server: A Different Animal
This scenario is not as pretty in IIS 5 on Windows 2000 Server (and in 5.1 in Windows XP). The IIS process (inetinfo.exe) runs as SYSTEM, so any in-process application runs as SYSTEM as well. We are not going to cover what happens when we run in-process as SYSTEM in IIS 5, because it is just a bad idea and it opens up gaping security holes. Most administrators already know this and tend to run their IIS applications out of process.
Out-of-process applications run as IWAM_MACHINE (using dllhost.exe). IIS 5 always impersonates the user first (even the anonymous account, IUSR_MACHINE). This means that although the process might be running as either SYSTEM or IWAM_MACHINE, the identity we see will always be, at a minimum, the anonymous account (IUSR_MACHINE) or the client's identity when using Integrated Windows Authentication (IWA). Since we don't have the option of running the IIS process under another account, we can take advantage of this impersonation and use it to effectively run IIS under another account. We do this by changing the anonymous account from IUSR_MACHINE to our service account configured with the rights we require for our application. Now, that covers only IIS's identity at this point. Since ASP.NET runs in a worker process (not in-process, as IIS 6 does), it is configured with its own identity. We can simply use impersonation in ASP.NET with anonymous allowed and it will impersonate the IIS anonymous account, which in our case will be the service account. This gets around the undesirable situation of storing credentials. It is not as straightforward as using an application pool in Windows 2003, but it accomplishes the same thing.
The problem with this last scenario in Windows 2000 of impersonating the anonymous account is that we may wish to use IWA to identify our users. This is not compatible with our solution. To rectify this, we can instead configure ASP.NET's process account rather than IIS's anonymous account. This identity is configured in the machine.config file under the tag. If we set explicit credentials for the userName and password values, we will change the process account that ASP.NET runs under. This is not as desirable as impersonating the anonymous account, because we have the additional complication of storing the credentials securely.
A problem with both of these solutions is that they affect all web applications on the machine. If we are running only a single ASP.NET application per server, it is not a problem to configure everything with the same identity. However, if different applications need to run with different identities, neither of the previous two solutions will work. This usually gives rise to an attempt to configure the identity of the ASP.NET application specifically using the web.config file and the tag. The syntax looks like this:
Warning: Do Not Put Plaintext Credentials in Configuration Files!
We hope by now that this is relatively obvious, but it is a bad idea to put plaintext credentials in configuration files. If we must store secrets, we should use one of the available technologies for encrypting them appropriately. ASP.NET 2.0 has new configuration encryption features, and .NET 1.x has the aspnet_setreg.exe tool (available as a download from Microsoft) for accomplishing the same goal. If we are storing credentials in our own configuration section, many additional alternatives are available.
We are going to warn developers away from this (most cannot even get it to work). This is not nearly as desirable as impersonating the anonymous account or changing the tag because of limitations in Windows 2000. Specifically, we essentially need to run our ASP.NET worker process as SYSTEM in order for it to run as our configured identity in web.config. This is because beneath the covers, it is performing a call to LogonUser to create the token for the service account it will impersonate. Windows 2000 requires the "Act as part of the operating system" (SE_TCB_NAME) privilege in order to use LogonUser. This privilege is granted only to the SYSTEM account by default. Also by default, the tag is set to run as the ASPNET user. We would have to run our ASP.NET process as SYSTEM or grant the TCB privilege to the ASPNET user in order for this to succeed. Most developers never figure this out and just give up after they get errors. It is probably just as well; remember what we said earlier about running in-process with IIS 5? We panned that idea because it required our web applications to run as SYSTEM. It is the same issue here.
Our last approach involves using explicit credentials for a service account and supplying those directly to our DirectoryEntry objects. Why would we take this approach rather than one of the other approaches? As it turns out, not every web server will be deployed as a domain member server. In some organizations, IT policy restrictions prevent this type of deployment. In this case, we still need a viable solution. Note that in order for a deployment like this to work, the web server will still need appropriate network access to the domain controllers.
The net result of this last approach is that we will store the credentials for the service account somewhere in our configuration system. Again, please heed our warnings and store the credentials securely!
Example Approaches for the Delegation Model
We cited two approaches for using the delegation model. Of these two, the approach using Kerberos delegation is probably the one that web application developers want to use most often as well as the one that gives them the most trouble, so we will focus on it first. There are really four main requirements to get this to work correctly.
Let's take each of these in turn.
Configuring IWA and Kerberos
Configuring our web application to use IWA is not hard, but sometimes getting Kerberos to work can be tricky. IWA actually uses the Negotiate protocol by default to authenticate users, so it can negotiate down to NTLM if Kerberos is not available. We do not want this! However, getting Kerberos authentication to work is usually not a big deal. The thing we forget most often is that if we are accessing the web site with a DNS name that is different from the machine name, we need to set explicit service principal names (SPNs) on our process account in Active Directory that advertise the service as using the additional DNS name. Once again, see The .NET Developer's Guide to Windows Security for more details on SPNs.
Configuring the Process Account
Configuring the process account for delegation is typically done by an administrator using the Active Directory Users and Computers (ADUC) MMC. Note that the process account we are using must be a domain account!
If our web applications are configured to run under local accounts (either the ASPNET user or NETWORK SERVICE, depending on the platform), the computer account for our IIS server must be configured for delegation, instead of a specific service account. If we chose to run the process under a different identity (perhaps by using an application pool identity), then we should configure this account for delegation. Unlike the computer accounts in Active Directory, any service account we create will not have an SPN by default. We must use a tool like SetSPN.exe to create an SPN before Kerberos will work correctly.
Ideally, we would like to use the constrained delegation feature of Windows Server 2003 Active Directory to limit which services our web server account may delegate to (our Active Directory servers in this case). However, that may not be an option if we are using Windows 2000 Server. In that case, we must use unconstrained delegation.
Configuring ASP.NET Impersonation
This part is definitely easy. We simply need to ensure that our web.config file includes this in the section:
This ensures that the current ASP.NET request thread will be impersonating the user we authenticated. Note that we can also impersonate programmatically using the WindowsIdentity.Impersonate method if we wish, creating a WindowsIdentity object from the Identity property of the HttpContext.User property. This is more complex, but gives us the flexibility to use impersonation only where needed.
Using Default Credentials. This part is also easy. Here, we are just specifying null/Nothing for our usernames and passwords and are using AuthenticationTypes.Secure, as we have already discussed. Our DirectoryEntry will use the impersonated identity of the authenticated user when executing the LDAP bind, and because we have enabled Kerberos delegation correctly, the user's security context will be forwarded successfully to the directory!
The Finer Points of Kerberos Delegation
We have tried to provide a prescriptive recipe to demonstrate how to build a delegated model ASP.NET application to access Active Directory. Because this book is not explicitly about Windows security and Kerberos, we may have glossed over some finer points. However, we have tried to provide a variety of references to other materials to get more information, and we have provided some helpful troubleshooting information.
An Alternate Approach: Delegation with Explicit Credentials
We mentioned that we also can build a delegated model application using explicit credentials. While this approach is not as good from a security standpoint, as it requires that we know the user's plaintext credentials, sometimes we must use it. For example, our web server may not be a domain member server due to IT policy restrictions on how it is deployed. We may also need to delegate to a system that does not use Windows security, such as an ADAM instance that uses ADAM users.
In this case, the delegation is actually fairly straightforward. We must collect the plaintext credentials from the user (typically as part of authentication) and then use the collected credentials in our DirectoryEntry constructors when accessing the directory. Here are the key points to remember.
Because we are not relying on Windows security to protect the user's credentials, the burden is on us to do this well, and we must not take this additional responsibility lightly. We are already dealing in plaintext credentials, which are inherently less secure than their Windows counterparts are, so we are starting at a disadvantage.
Serverless Binding and ASP.NET
If we remember from Chapter 3, serverless binds require a domain security context to work properly. Because of this, serverless binding is appropriate for use only in ASP.NET applications where we are supplying default credentials. We have already discussed the approaches that will use default credentials and will work properly. We should remember that if our chosen approach requires us to supply explicit credentials, it is likely that we will also need to supply some form of server name. Ideally, for Active Directory, we will use the DNS domain name of the domain so that we can still take advantage of the Locator (see Chapters 3 and 9). If we are using another server, such as ADAM or a non-Microsoft LDAP directory, an explicit server name must be specified.
Binding with ADAM
Binding with ADAM is interesting and bears special mention. Unlike Active Directory, where we can bind with a user's credentials using either a simple bind or a secure bind, ADAM only supports simple binds for users in the ADAM store itself.[4]
[4] This is not actually completely true. The latest version of ADAM also supports Digest authentication for ADAM users. However, SDS does not (currently) have explicit support for Digest authentication, so we are limited to simple binds for SDS. If we use System.DirectoryServices.Protocols (SDS.P), then we can specify Digest authentication directly and take advantage of this feature.
This seems simple enough, except for one additional fact: ADAM also supports the ability to authenticate Windows users. A Windows user here means a user on the local machine that is hosting ADAM, or a user in any domain with which the server has a trust relationship (Active Directory or a Windows NT4 domain). There are two ways to authenticate a Windows user via a bind to ADAM.
"Pass-Through" Authentication to Windows
What does this mean? In the first case, where we do a secure bind to ADAM to authenticate the user, ADAM is using a mechanism called passthrough authentication. It essentially just takes the incoming secure bind request and passes it off to the host server to authenticate the user. If that is successful, the user is authenticated. Note that the user in this case does not have a bindable object in the ADAM store representing her. Her actual Windows account was authenticated. The user can be a member of a group in ADAM as a foreign security principal, but she is not an ADAM user (a security principal).
Bind Proxies
The second case involves what is called a bind proxy object. Here, a special class of object called a userProxy (naturally) is created in ADAM. It has a SID attribute that points to the SID of the Windows principal to be authenticated. The simple bind is accepted by ADAM and is redirected to the server for the actual authentication again. ADAM ships with the userProxy class as an optional class in an LDIF file. In actuality, any class that contains the msDS-bindProxy auxiliary class can act as a bind proxy. It just happens that it is usually easiest to add the optional userProxy class.
Bind proxies are intended primarily to support applications that cannot do a secure bind. Many non-Microsoft LDAP APIs do not support GSSSPNEGO, so this mechanism provides some common ground.
The other scenario that bind proxies support is the case where the consuming application requires additional schema on the user object that cannot be added directly to Active Directory for either technical or political reasons. In this case, the bind proxy object has its schema extended to include the additional required attributes. Then the user can authenticate against ADAM using his Active Directory password, but the application can look in ADAM to get the additional data needed.
For more information about bind proxies, we highly recommend reading the ADAM Technical Reference.[5]
[5] www.microsoft.com/downloads/details.aspx?FamilyID=96C660F7-D932-4F59-852C- 2844B343F3E0&displaylang=en
Binding and Other Directories
As we already discussed, SDS really exposes two types of binds: the simple bind, specified with AuthenticationTypes.None, and the GSS-SPNEGO SASL bind, supported by AuthenticationTypes.Secure. Since it is unlikely that a non-Windows LDAP directory will support the Windows Negotiate protocol (none of them does, to our knowledge), the simple bind is the way to go.
The only real trick with the simple bind is getting the username in the correct format. The RFC states that the user's distinguished name (DN) must be supported, so that is always a good choice. Some directories may also support other naming mechanisms that may be used with a simple bind (Active Directory supports the User Principal Name [UPN] and NT logon name, for example), so consult your server's reference for more details.
Securing the Simple Bind
In order to secure the bind, we must secure the channel. The only method SDS supports is to use the AuthenticationTypes.SecureSocketsLayer flag. This will use SSL to encrypt the channel, but it requires a valid server certificate on the server that the client trusts. This also generally means that we need to use a proper DNS for the server name, as an SSL handshake generally requires that the name requested by the client matches the subject alternative name field of the certificate. We provide some additional troubleshooting tips and details for SSL binds back in Chapter 3 that apply equally to Active Directory and ADAM.
It is also possible to encrypt the channel with some sort of external channel encryption, such as IPSEC, if SSL is not an option.
Client Certificate Authentication
One feature of SSL is that in addition to authenticating the server's identity via its certificate, it can optionally authenticate the client if the client has a certificate and the server is configured to accept certificates. Since LDAP on Windows supports SSL through the same mechanism that other parts of Windows use for SSL (namely, the Schannel SSPI provider), it is natural to wonder how client certificates are supported with LDAP and whether they can be used to authenticate the client.
The good news here is that LDAP fully honors requests for client certificates and will use them if the client code has a valid certificate available (with access to its private key). The question then becomes whether a client certificate authentication can be used as a bind to Active Directory or ADAM.
The answer here is yes for Active Directory. If the client provides a client certificate, the LDAP API will use the EXTERNAL SASL mechanism under the hood to bind the user to Active Directory if an appropriate mapping between the certificate identity and a Windows user can be found. Note that this feature is not documented at all and is rarely used, so there is little practical experience with it. However, if we can get client certificate authentication working with IIS, we should be able to do the same for LDAP. Supposedly, ADAM also supports client certificate authentication, but we were unable to verify that in our own testing, or learn how to configure it.
Binding Features Not Supported by SDS/ADSI
Even though the LDAP API, Active Directory, and ADAM support a variety of SASL mechanisms, the only one currently available for use with ADSI and SDS is GSS-SPNEGO via the AuthenticationTypes.Secure flag. If we want to use one of the other methods, such as Digest authentication, we are basically out of luck. Digest is especially tempting for ADAM developers, because it allows a secure binding protocol without the additional trouble of having to secure the channel via SSL.
What is a .NET developer to do? If we absolutely need to have Digest authentication or a different SASL mechanism, we must unfortunately abandon SDS and look at the SDS.P namespace. Because there is no built-in mechanism to interoperate between the two APIs, it is an all or nothing proposition.
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