I am hosting a WCF Web Service with IIS 6.0. My application pool is running under a local Administrator account, and I have other local users defined for accessing the Web Service. I've written the following code to validate users:
//Any public static (Shared in Visual Basic) members of this type are thread safe
public static PrincipalContext vpc;
//static initializer
static UserManagement()
{
vpc = new PrincipalContext(ContextType.Machine);
}
//Determines whether the given credentials are for a valid Administrator
public static bool validateAdminCredentials(string username, string password)
{
using (PrincipalContext principalContext = new PrincipalContext(ContextType.Machine))
{
if (vpc.ValidateCredentials(username, password))
{
using (UserPrincipal user = UserPrincipal.FindByIdentity(principalContext, IdentityType.SamAccountName, username))
{
foreach (GroupPrincipal gp in user.GetGroups())
{
try
{
if (gp.Name.Equals("Administrators"))
{
return true;
}
}
finally
{
gp.Dispose();
}
}
}
}
return false;
} //end using PrincipalContext
}
...and in another class:
//verify the user's password
if (!UserManagement.vpc.ValidateCredentials(username, password))
{
string errorMsg = string.Format("Invalid credentials received for user '{0}'", username);
throw new Exception(errorMsg);
}
(Note: I am using the public static PrincipalContext (vpc) solely for calling the ValidateCredentials method; I create a different, temporary PrincipalContext for creating, deleting, and finding users and groups, as I got various COM-related errors when I tried using the global PrincipalContext for everything).
So, most of the time, this code works wonderfully. However, intermittently, I get the following error:
Multiple connections to a server or shared resource by the same user, using more than one user name, are not allowed. Disconnect all previous connections to the server or shared resource and try again. (Exception from HRESULT: 0x800704C3)
Application: System.DirectoryServices.AccountManagement
Stack Trace: at System.DirectoryServices.AccountManagement.CredentialValidator.BindSam(String target, String userName, String password) at System.DirectoryServices.AccountManagement.CredentialValidator.Validate(String userName, String password) at System.DirectoryServices.AccountManagement.PrincipalContext.ValidateCredentials(String userName, String password) at MyNamespace.User..ctor(String username, String password)
Once the error occurs, I continue to get it until I restart my entire server (not just IIS). I've tried restarting my application pool and/or IIS, but the error does not go away until I restart the machine. I've also tried instantiating (via a using block) a new PrincipalContext for every call to ValidateCredentials (which I shouldn't have to do), but I still eventually get the same error. From what I've read on System.DirectoryServices.AccountManagement (msdn docs, articles), I believe I'm using it correctly, but this error is crippling my application! I need (and should be able) to validate local user credentials from web service requests coming from multiple clients. Am I doing something wrong? Any help on solving this issue would be much appreciated...
"Any public static (Shared in Visual Basic) members of this type are thread safe" This boilerplate text confuses a lot of people. It means that any static members exposed by the type are thread-safe. It doesn't mean that any instance of the type stored in a static member will be thread-safe.
Your validateAdminCredentials method creates a new PrincipalContext object, but then proceeds to use the static vpc instance to validate the credentials. Since you have no locks around the access to the static instance, and the instance methods are not thread-safe, you'll eventually get two threads trying to access the same instance at the same time, which will not work.
Try removing the vpc field and using the principalContext instance to validate the credentials. You'll need to create and dispose of the PrincipalContext on every call.
Also, rather than manually iterating the user's groups, you can use the IsMemberOf method to test for membership in a group.
public static bool ValidateCredentials(string username, string password)
{
using (PrincipalContext principalContext = new PrincipalContext(ContextType.Machine))
{
return principalContext.ValidateCredentials(username, password);
}
}
public static bool validateAdminCredentials(string username, string password)
{
using (PrincipalContext principalContext = new PrincipalContext(ContextType.Machine))
{
if (principalContext.ValidateCredentials(username, password))
{
using (UserPrincipal user = UserPrincipal.FindByIdentity(principalContext, IdentityType.SamAccountName, username))
using (GroupPrincipal group = GroupPrincipal.FindByIdentity(principalContext, IdentityType.SamAccountName, "Administrators"))
{
if (null != group && user.IsMemberOf(group))
{
return true;
}
}
}
return false;
}
}
My suggestion would be to not use ValidateCredentials with the SAM provider. I believe it should work, but I am also not surprised that it runs into problems. I think you'll do better by doing a p/invoke into LogonUser using Network logon type for programmatic credential validation.
I'd like to see if Microsoft has a known issue and solution to this problem but in the meantime I'd suggest just coding around it.
If you need a solution the works even with Windows 2000 and w/o running code as system you could also use this:
public static bool ValidateUser(
string userName,
string domain,
string password)
{
var tcpListener = new TcpListener(IPAddress.Loopback, 0);
tcpListener.Start();
var isLoggedOn = false;
tcpListener.BeginAcceptTcpClient(
delegate(IAsyncResult asyncResult)
{
using (var serverSide =
new NegotiateStream(
tcpListener.EndAcceptTcpClient(
asyncResult).GetStream()))
{
try
{
serverSide.AuthenticateAsServer(
CredentialCache.DefaultNetworkCredentials,
ProtectionLevel.None,
TokenImpersonationLevel.Impersonation);
var id = (WindowsIdentity)serverSide.RemoteIdentity;
isLoggedOn = id != null;
}
catch (InvalidCredentialException) { }
}
}, null);
var ipEndpoint = (IPEndPoint) tcpListener.LocalEndpoint;
using (var clientSide =
new NegotiateStream(
new TcpClient(
ipEndpoint.Address.ToString(),
ipEndpoint.Port).GetStream()))
{
try
{
clientSide.AuthenticateAsClient(
new NetworkCredential(
userName,
password,
domain),
"",
ProtectionLevel.None,
TokenImpersonationLevel.Impersonation);
}
catch(InvalidCredentialException){}
}
tcpListener.Stop();
return isLoggedOn;
}
I resolved this same problem by using the domain context. It is possible to use the machine context to delegate to the domain, but it appears that you will be forced to run into this problem eventually.
Note that you should create the domain context fresh for each call, because it must be created using the domain of the user you want to authenticate.
My application needed to support machine users as well. I found that whenever a user was a machine user, the domain context would throw an exception. If I catch the exception, I work against the machine context.
User contributions licensed under CC BY-SA 3.0