Authentication Using SDS
One of the easiest methods available to us is to simply create a DirectoryEntry object using our client's credentials and take some action that will cause a bind. If no error occurs, we have successfully bound to the directory and we can infer that the client's credentials are valid. However, the opposite is not quite true. That is, if an error occurs, we cannot automatically assume that the client's credentials were invalid. An error could occur because of other things, such as account lockout, being disabled, or log-on hours such that the client might not be able to authenticate successfully.
Before we get too deep into this discussion, we would really like to point out that using the DirectoryEntry object strictly for authentication is actually a nonoptimal solution. It is definitely easy, but there are a number of problems with using it. First, the DirectoryEntry object was not conceived for authentication use. In high-volume applications, we will find that using DirectoryEntry is impractical, as it cannot scale. In Chapter 3, we discussed how ADSI uses connection caching to reuse existing LDAP connections. Creating many ADSI objects with different sets of credentials works directly against ADSI's caching strategy and will cause us to run out of TCP/IP wildcard ports, resulting in seemingly random errors when trying to connect to the directory. This problem is especially insidious, as we often get great results during development, but things fall apart when we deploy to our production environment. Do not even consider using ADSI for authentication if the application requires high scalability with many concurrent users. It will not work.
It is also not the best-performing method, as there is a cost to building the schema for an object that will only be thrown away. For small or single-user implementations, the cost is probably tolerable, but using this technique, for instance, for larger ASP.NET web sites is probably a nonstarter.
Keeping in mind that the username can come in different formats, we should make sure that we have accounted for these. For a non-SSL Active Directory installation, the credentials can be passed securely using AuthenticationTypes.Secure. However, we are restricted to using the UPN format, or NT Login format, as the username. For ADAM, we can use AuthenticationTypes.None or SecureSocketsLayer (if SSL is configured). The username should be in DN format or UPN format for ADAM security principals. The UPN format for ADAM actually can vary widely, from the more common email format (user1@adaminstance.com) to simply a username or UID format (user1).
Warning: Always Add Additional Security to Simple Binds
The username and password will be sent on the network unencrypted and vulnerable when using AuthenticationTypes.None. Always protect the credentials using SecureSocketsLayer in ADAM. If you have read this book cover to cover, you have probably already been warned about this a dozen times already. We apologize for beating you over the head, but simple binds are just not safe enough to let this go unmentioned.
Active Directory Authentication
Listing 12.1 demonstrates how we can use the DirectoryEntry object with Active Directory to authenticate credentials. The implementation shown is a fairly naïve implementation using RootDSE, but it is adequate for a variety of situations.
Listing 12.1. A Naïve Active Directory Authentication Method
const int ERROR_LOGON_FAILURE = -2147023570; private bool AuthenticateUser( string username, string password, string domain) { //optionally add the domain string adsPath = String.Format( "LDAP://{0}rootDSE", (domain != null && domain.Length > 0) ? domain + "/" : String.Empty ); DirectoryEntry root = new DirectoryEntry( adsPath, username, password, AuthenticationTypes.Secure | AuthenticationTypes.FastBind ); using (root) { try { //force the bind object tmp = root.NativeObject; return true; } catch (System.Runtime.InteropServices.COMException ex) { //some other error happened, so rethrow it if (ex.ErrorCode != ERROR_LOGON_FAILURE) throw; return false; } } } |
We recommend RootDSE as the object to use in our ADsPath to test the bind. The reason for this is that the object is already known and is available anonymously, so no authorization demands are placed on reading it. We can be fairly certain that the underlying bind will simply test the user's credentials.
We mentioned earlier that using something like Listing 12.1 in a large ASP.NET site to authenticate credentials would probably not be a good idea. It is relatively slow for very high-volume applications and it tends to scale poorly. It is most appropriate for fat-client types of applications where we don't have to worry as much about other users or scaling requirements.
ADAM Authentication
We can just slightly modify our previous example in order to authenticate users in ADAM or bind proxies to Active Directory users using an LDAP simple bind. We will need to use the name of the server in conjunction with RootDSE in order to find our ADAM instance. Additionally, the domain parameter for our AuthenticateUser method does not make sense for ADAM. If we simply rename "domain" to "server" and fix up the references a bit, we are almost there. The last minor change will be to use AuthenticationTypes.SecureSocketsLayer in order to protect any credentials and to switch to a simple bind, as discussed in Chapter 8. With only a few minor changes, we have a working ADAM authentication mechanism using System.DirectoryServices (SDS), as Listing 12.2 demonstrates.
Listing 12.2. ADAM Authentication Using SDS
const int ERROR_LOGON_FAILURE = -2147023570; private bool AuthenticateUser( string username, string password, string server) { //optionally add the domain string adsPath = String.Format( "LDAP://{0}/rootDSE", server ); DirectoryEntry root = new DirectoryEntry( adsPath, username, password, AuthenticationTypes.SecureSocketsLayer | AuthenticationTypes.FastBind ); using (root) { try { //force the bind object tmp = root.NativeObject; return true; } catch (System.Runtime.InteropServices.COMException ex) { //some other error happened, so rethrow it if (ex.ErrorCode != ERROR_LOGON_FAILURE) throw; return false; } } } |
The user's distinguished name (DN) is typically the name that we use with ADAM for authentication. However, our clients will most likely not remember or even know their DN. To cope with this, we can assign users a log-on name and store it in ADAM, using the userPrincipalName attribute. Once this attribute has been assigned, we can then use it as the username for our ADAM users. It can be passed instead of the DN for authentication in the DirectoryEntry object.
What if we want to use ADAM's pass-through authentication mechanism to authenticate Windows users? We discussed pass-through authentication in Chapter 8 and mentioned that to use it, we simply need to use a secure bind rather than a simple bind. As it turns out, Listing 12.1 will basically work for this if we specify the ADAM instance name correctly in our ADsPath, so we are already covered.
If we need to support both Windows and ADAM users, then we need to handle both secure and simple binds in our code. This can be tricky, as we essentially need a way to differentiate our users based on the username they supply so that we know which approach to take, or that we need to try both. Given that naming conventions will tend to vary from implementation to implementation, we do not have a specific sample to recommend beyond a combination of Listings 12.1 and 12.2. However, we do recommend choosing a naming convention for users in Active Directory and ADAM that makes this easy. A little planning will go a long way here.
Authentication Using SDS P
|