Using the System.DirectoryServices.Protocols (SDS.P) namespace, we have access to a number of features that are not available using ADSI or SDS. We have much tighter control over the binding process and can more efficiently manage connections to the directory. This allows us to more easily scale where the previous SDS technique using DirectoryEntry would fall flat. If we recall from Chapter 3, DirectoryEntry will create a new connection to the directory for every permutation of credentials and AuthenticationTypes. Since we are specifically changing credentials for authentication, obviously this equates to creating a new connection for each unique authentication request.
SDS.P also gives us access to a feature called fast concurrent binding. Unlike a normal LDAP bind, this type of bind does not attempt to build a security token for the credentials. It simply validates the username and password. This allows the bind operation to complete in a fraction of the time a normal bind operation would require. This can yield important performance benefits, which may be especially important in server applications like ASP.NET.
Fast concurrent binding is available to us with Windows Server 2003 and ADAM when using a simple bind (AuthType.Basic). It cannot be combined with a secure bind. As such, we must take care, as usual, to encrypt the channel with SSL or another appropriate mechanism. Additionally, the client itself must be Windows Server 2003. While this sounds restrictive, in reality we typically would be using fast concurrent binding in server applications such as IIS, so deployment to Windows Server 2003 is reasonable. Developers will not be able to test this feature on Windows XP workstations, though.
Using the LdapConnection class, we will walk through a sample of how this can be done. Starting with Listing 12.3, we will bind to a server and start the process of determining what features we can use.
Listing 12.3. Setting LdapConnection Options
class LdapAuth : IDisposable { LdapConnection connect; bool fastBind; bool useSSL; bool isADAM; public LdapAuth(string server, bool useSSL) { this.useSSL = useSSL; this.fastBind = false; this.isADAM = false; this.connect = new LdapConnection( new LdapDirectoryIdentifier(server), null, AuthType.Basic ); this.connect.Bind(); this.connect.SessionOptions.ProtocolVersion = 3; this.connect.SessionOptions.SecureSocketLayer = this.useSSL; CheckCapabilities(this.connect); if (this.fastBind) { try { this.connect.SessionOptions.FastConcurrentBind(); } catch (PlatformNotSupportedException) { //this will happen when clent is not W2K3 this.fastBind = false; } } if (!this.fastBind && !this.useSSL && !this.isADAM) { //we did not get a fast bind or //SSL so we can try to at least //encrypt the credentials for Active Directory this.connect.AuthType = AuthType.Negotiate; this.connect.SessionOptions.Sealing = true; this.connect.SessionOptions.Signing = true; } if (this.isADAM && !this.useSSL) { //we are using ADAM with no SSL //try to use Digest to secure bind //Requires Win2k3 R2 ADAM this.connect.AuthType = AuthType.Digest; } } |
Listing 12.3 is first binding to a server we provide, using a simple bind and SSL if it was specified. Ideally, we would want to use SSL here for both ADAM and Active Directory, as that in combination with a fast concurrent bind would provide the best performance while still protecting the credentials. If we are using Active Directory without SSL, then we would want to use AuthType.Negotiate (similar to AuthenticationTypes.Secure in SDS), along with Signing and Sealing options to protect the credentials. Lastly, if we were using ADAM without SSL, we would want to try to use Digest authentication. While SDS does not support Digest authentication with ADAM, it is supported with the lower-level control we have in SDS.P. The only caveat here is that it requires the Windows Server 2003 R2 release of ADAM as well as Windows 2003 SP1 clients and servers. We are intentionally not showing ADAM authentication without some sort of credentials protection (transport or otherwise), because it is just a bad idea and we would not want to see this in any type of production application.
To set all the connection options in Listing 12.3, we needed to know a bit more about the directory we were working with. Listing 12.4 contains our helper method used in our LdapAuth class. It demonstrates how we can read the RootDSE object using SDS.P to determine both the directory type and whether the server supports fast concurrent binding.
Listing 12.4. Determining Server Capabilities
private void CheckCapabilities(LdapConnection conn) { string ext = "supportedExtension"; string cap = "supportedCapabilities"; SearchRequest request = new SearchRequest( null, //read the rootDSE "(objectClass=*)", SearchScope.Base, new string[] { ext, cap } ); //set 120 second timelimit request.TimeLimit = TimeSpan.FromSeconds(120); SearchResponse response = (SearchResponse)conn.SendRequest(request); if (response.ResultCode != ResultCode.Success) throw new Exception(response.ErrorMessage); SearchResultEntry entry = response.Entries[0]; object[] vals = entry.Attributes[ext].GetValues(typeof(string)); foreach (string s in vals) { //OID for Fast Concurrent Bind support if (s == "1.2.840.113556.1.4.1781") { this.fastBind = true; break; } } vals = entry.Attributes[cap].GetValues(typeof(string)); foreach (string s in vals) { //OID for ADAM if (s == "1.2.840.113556.1.4.1851") { this.isADAM = true; break; } } } |
The first parameter to the SearchRequest object in Listing 12.4 specifies that we want to read the RootDSE object. We are checking two attributes, called supportedExtension and supportedCapabilities, to determine if fast concurrent binding is supported and if the directory we are using is ADAM, respectively.
Once we have determined what options are available to us and we have set our connection options, we can simply attempt a bind to the directory, as Listing 12.5 shows. We should also take care to clean up our LdapConnection instance at some point, although we probably want to keep it open for an extended period and reuse it repeatedly.
Listing 12.5. LDAP Authentication
public bool Authenticate(NetworkCredential credentials) { try { this.connect.Bind(credentials); return true; } catch (LdapException ex) { if (ex.ErrorCode != 49) throw; return false; } } |
NetworkCredential can be in any of the formats we covered previously in this chapter and in Chapter 3. We should also note that the LdapConnection.Bind method is thread safe, making it safe to use in multithreaded applications. We are omitting the final pieces to our LdapAuth class that implement the IDisposable pattern, but all of this code can be found on this book's web site.
For those who plan to use LDAP authentication in their application, we highly recommend an approach like this one that uses SDS.P. The ability to manage our LDAP network connections directly will allow our application to scale properly to many concurrent users, and the ability to use fast concurrent binding can give us an important performance boost.
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