Validating Users in Active Directory Gotcha
A while ago I asked about doing Active Directory Authentication, after getting some advice, I settled on using the Acitve Directory Membership provider. It worked, but after a while we started to get really bad feedback from the users about the time that it took to login. To give you an idea, I timed it and I have an average of 14 seconds spent just making the "Membership.ValidateUser()" call.
For a while I insisted that it can't be my code, after all, I was explicitly using the Microsoft recommended way of doing it. I am not an Active Directory / LDAP expert by any mean, so I just went with the recommended appraoch. Today the network guys finally hunt me down and show me the sniffer trace logs, which shows my application getting a response packet from the domain controller and spending 12 seconds doing something with it, before responding.
I sat down and wrote the following benchmark.
Validate using MembershipProvider:
private static bool MembershipProvider(string user, string password)
{
try
{
return Membership.ValidateUser(user, password);
}
catch (Exception e)
{
Console.WriteLine(e);
return false;
}
}
<system.web>
<membership defaultProvider="ExtranetADMembershipProvider">
<providers>
<add name="ExtranetADMembershipProvider"
type="System.Web.Security.ActiveDirectoryMembershipProvider, System.Web"
connectionStringName="ADConnectionString"
attributeMapUsername="SAMAccountName"
connectionUsername="MyDomain\Beaker"
connectionPassword="Do you get the joke?"/>
</providers>
</membership>
</system.web>
Validate directly using System.DirectoryServices:
private static bool DirectLdap(string user, string password)
{
try
{
using (DirectoryEntry de = new DirectoryEntry(ldapPath,
Domain +"\\"+ user, password,
AuthenticationTypes.Secure))
{
return de.NativeObject != null;
}
}
catch (Exception e)
{
Console.WriteLine(e);
return false;
}
}
The end result is rather suprising:
LDAP:
First: 00:00.4531424
After first time, validate 1000 times same user: 00:20.9851808
Membership:
First: 00:24.7040736
After first time, validate 1000 times same user: 00:24.3603104
So, for the first request I made, it is 55 times faster to use the direct approach. and it is 16 precent slower on repeating calls. I have run the test a number of times, and the result of 0.4 seconds is actually very close to the high end for the first request.
My actual usage scenario meant that the average login time was actually averaging on 14 seconds or so. I would guess that the issue is some sort of caching that it is doing, but the initial cost is so high, and apperantly there is continous cost associated with it that is visible over long term usage.
I am using it for authenticaton only, so I just dropped the whole thing and use DirectoryServices myself. I am very disappointed about it, but the perf has shot thourgh the roof.
Comments
Very strange. It almost sounds the Membership providers just caches all users in Ldap. Have you tried validating different users after each other?
If the caching is so costly, why do you even want caching? I think almost nobody validates the same user 1000 times, or even more than 10 times a day.
I have also noticed that the performance of the System.DirectoryServices implementation is less than it should. For searching and reading attributes LDAP Administrator (by Softerra) is significantly faster than any .NET application I have written that uses System.DirectoryServices for LDAP communication. Maybe some COM Interop and Marshalling is hindering performance.
Hi Ayende,
I'm having the same problem with one of our applications. It is taking 15-20 seconds to load the first page, and then it works normally.
This was on my list of items to fix, so thanks for writing this post! :)
If you care, I believe the full source for the built in providers is available. ScottGu mentioned it in this post:
http://weblogs.asp.net/scottgu/archive/2006/04/13/442772.aspx
You may find something in the ActiveDirectory provider that you can resolve.
I'm pretty sure I can answer this for you .. I think it's because you've specified secure channel, but you haven't got SSL available at the AD GC/DC .. It's probably waiting for a timeout while looking for a secure channel using SSL, then trying sign-and-seal security (after 10-20 seconds .. this timeout value is probably down to lots of factors). This value "SignAndSeal" does then get cached in a static DirectoryInfo wrapper in the ADMembershipProvider, for future calls during the AppDomain's life.
http://msdn2.microsoft.com/en-us/library/system.web.security.activedirectorymembershipprovider.aspx
Take a look at the bit under "Active Directory connections" .. I've not encountered this, as I'm using ADAM instead of AD but pretty sure that's your problem.
I believe the Secure option on DirectoryEntry just goes straight to sign-and-seal security, hence the reason it's faster.
No, there is no data caching going on .. The only thing that gets cached at startup by the AD membership provider (apart from the setting as above) seems to be exceptions (boy is this class full of crap code! ).. try it - take down the directory server, start your web app, and access some page that invokes the AD Membership Provider, exception .. now try starting the directory server, and accessing the AD Membership Provider .. nope still an exception, needs an AppDomain restart to recover .. (because the server down exception is cached in a static private field).
And to save you some time .. No, the source for none of the ActiveDirectory Providers are available .. But thank god for Reflector! Believe me I've just had to hack in a load of stuff to basically fix a load of bad stuff in ActiveDirectoryMembershipProvider. It didn't take long to do but does have had to some very dodgy reflection with private variables to achieve it.
Just for your reference.. The reason for this hack was the VERY poor performance of paging for the search methods in the ADMembershipProvider (MS knows this, so give you the ability to turn off the search functionality). Basically out the box, it does the old DataSet trick of loading ALL the records to the client , and doing client side paging. With one line of additional code in the FindUser methods, you can speed this up substantially by enabling server side paging (on certain Windows versions/.NET 2.0). Boy have I got to send the patch to MS for this one!
If anyone is interested in this, let me know.
Oh forgot to mention .. you can probably speed your DirectoryEntry ValidateUser code up a magnitude by using "FastBind | Secure" in the options .. This is recommended for any Authentication only scenario, as it doesn't check that the object (i.e. your root DN) exists, it just does the authentication part.
After a bit of reading, it appears the "Secure" flag on DirectoryEntry uses SSPI authentication, not SSL .. I'm willing to bet if you set that flag to UseSSL it'll take a long time!
http://www.joekaplan.net/CategoryView,category,Windows+Security.aspx#a9b25d233-a484-4905-a60b-8bb8748c7068
BTW Joe Kaplan is THE man when it comes to AD related stuff ..
Sorry got that slightly wrong! (ah, the bad combination of not actually reading the link I sent, trying to remember something from ages ago, and a few beers B-) ) ..
Fast Concurrent Binding is the boy .. not the FastBind flag - easily confused.
Oh and it's also possible that the SSL (SChannel?) runs on a different port number, if you're not using Win2K3.
You might want to look into using the LdapConnection class in the System.DirectoryServices.Protocols namespace. It's a bit more low level than the normal DirectoryServicies stuff but completely avoids ADSI entirely and gives you more control over how things work.
Dave,
If I set using SSL I immediately get a "server not operational error".
Even assuming that this is what is causing some of the problem, it shouldn't take so long.
Michael,
Thanks for the suggestion, but my main concerns here is just getting it done reasonably fast, I have little interest in digging deeper
Oren,
I'm using code similar to your DirectLDAP fuction. I'm currently just letting it perform the actual validation in the unit tests, but would prefer to mock it. I'm fairly new to Rhino.Mocks and was wondering if you could point me to an example of testing that code using Rhino.Mocks.
@Ayende
"... is getting it done reasonably fast, I have little interest in digging deeper."
I understand that and I wouldn't have suggested it otherwise. ADSI, which System.DirectoryServices is built on top of, is not the fastest thing in the world. This however, does not use ADSI and I would expect it to be faster, especially with some of the unique bind abilities, such as the Fast Concurrent Binding mentioned above (which is not available with System.DirectoryServices / ADSI).
It's always better to dig a little deeper than to dismiss things based upon little or no information.
I would put the code in
public interface IAuthoeticationService
{
bool IsValidLogin(string username, string password)
}
Michael,
I agree 100%, except that right now I like the motivation for this.
This is no longer a bottle neck, and I am not convinced that dropping the authentication time from a few hundred milliseconds is worth the time that I would need to invest in it.
Comment preview