We have an application that validate user credentials with our internal ActiveDirectory domain. To do so, it uses the PrincipalContext::ValidateCredentials method from the .NET Framework.
While investigating another issue, we discovered that this method return true even when the password is expired. This result in users being able to access one of our internet systems despite having an expired password for months, even years in some cases. This seems strange, and a severe security flaw that we need to fix now that we're aware of it.
I tried looking up online about this behavior, but so far I found nothing. As far as I could tell, this method is really supposed to reject credentials if there is anything wrong with the account. For example, it does return false when the account is locked.
I doubt that this a bug in the ValidateCredential method itself. Its been around too long for that. It's fairly simple to use, so I don't think we screwed up here. Here's our code :
using (PrincipalContext context = new PrincipalContext(ContextType.Domain, domainName))
{
bool valide = context.ValidateCredentials(userName, passWord);
// Remaining code omitted
}
So, what could be happening here? What could cause ValidateCredentials to accept expired password?
As it turns out, and it took digging into the .NET framework source code to find out why, the reason that ValidateCredentials allows expired password is because it tries simple bind over SSL and Negotiate, but when it does a simple bind it activate support for fast concurrent binds, which apparently only validate that the user has a valid enabled account and password.
Here an excerpt from the Active Directory Cookbook from O'Reilly about fast concurrent binds :
Concurrent binding, unlike simple binding, does not generate a security token or determine a user’s group memberships during the authentication process. It only determines if the authenticating user has a valid enabled account and password, which makes it much faster than a typical bind. Concurrent binding is implemented as a session option that is set after you establish a connection to a domain controller, but before any bind attempts are made. After the option has been set, any bind attempt made with the connection will be a concurrent bind.
Yeah, I know this book is very old, but I presume this part is still pretty much valid. The tests I did seems to confirm that, as ValidateCredentials would accept expired passwords, but not locked accounts for example.
So, how to prevent this? Pretty simple, actually :
using (PrincipalContext context = new PrincipalContext(ContextType.Domain, domainName))
{
bool valide = context.ValidateCredentials(userName, passWord, ContextOptions.Negotiate | ContextOptions.Signing | ContextOptions.Sealing);
// Remaining code omitted
}
The trick is to specify connection options, but in the ValidateCredentials method and NOT the PrincipalContext constructor, otherwise the options are ignored by ValidateCredentials and it will attempt the fast bind with the problematic behavior.
Thanks to Gabriel Luci for pointing me in the right direction.