Error when using PrincipalContext.ValidateCredentials to authenticate against a Local Machine? - c#

I have a WCF service which contains a Login method that validates a username and password against the local machine credentials, and after a seemingly random period of time it will stop working for some users.
The actual login command looks like this:
public UserModel Login(string username, string password, string ipAddress)
{
// Verify valid parameters
if (username == null || password == null)
return null;
try
{
using (var pContext = new PrincipalContext(ContextType.Machine))
{
// Authenticate against local machine
if (pContext.ValidateCredentials(username, password))
{
// Authenticate user against database
using (var context = new MyEntities(Connections.GetConnectionString()))
{
var user = (from u in context.Users
where u.LoginName.ToUpper() == username.ToUpper()
&& u.IsActive == true
&& (string.IsNullOrEmpty(u.AllowedIpAddresses)
|| u.AllowedIpAddresses.Contains(ipAddress))
select u).FirstOrDefault();
// If user failed to authenticate against database
if (user == null)
return null;
// Map entity object to return object and assign session token
var userModel = Mapper.Map<User, UserModel>(user);
userModel.Token = Guid.NewGuid();
userModel.LastActivity = DateTime.Now;
// Authenticated users are added to a list on the server
// and their login expires after 20 minutes of inactivity
authenticatedUsers.Add(userModel);
sessionTimer.Start();
// User successfully authenticated, so return UserModel to client
return userModel;
}
}
}
}
catch(Exception ex)
{
// This is getting hit
MessageLog.WriteMessage(string.Format("Exception occurred while validating user: {0}\n{1}", ex.Message, ex.StackTrace));
return null;
}
// If authentication against local machine failed, return null
return null;
}
This appears to work fine for a few days, then it will abruptly stop working for some users and throw this exception:
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)
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.LoginService.Login(String username, String password, String ipAddress) in C:\Users\me\Desktop\somefolder\LoginService.svc.cs:line 67
Line 67 is: if (pContext.ValidateCredentials(username, password))
I'm not sure if it matters or not, but the final line of the error message is the path of the VS solution on my development machine, not the path to the files on the production server.
When it fails, it only fails for some users, while others can continue to login just fine. The only thing I have found to temporarily fix the error is running iisreset. Stopping/starting the web site or recycling the app pool doesn't work.
I am unable to reproduce the error on demand. I've tried logging in with the same user from multiple sessions and IP addresses, logging in with different users from the same browser session at the same time, spamming the Login button to try and get it to run multiple times, etc but everything appears to work fine.
I can see from our logging that users have been successfully able to login in the past:
3/21/2013
o 9:03a I logged in
o 1:54p UserB logged in
o 1:55p UserA logged in
o 2:38p UserB logged in
o 3:48p UserB logged in
o 5:18p UserA logged in
o 6:11p UserB logged in
3/22/2013
o 12:42p UserA logged in
o 5:22p UserB logged in
o 8:04p UserB logged in
3/25/2013 (today)
o 8:47a I logged in
o 12:38p UserB tries logging in and fails. Repeated ~15 times over next 45 min
o 1:58p I login successfully
o 2:08p I try to login with UserB's login info and fail with the same error
The reason we authenticate against the local machine is because users have an account created locally for FTP access, and we didn't want to build our own custom login system or make our users remember two sets of credentials.
The code should only authenticate the user's credentials, and does not do do anything else with the user's credentials. There is no other code that uses System.DirectoryServices, no file IO going on, and no access to anything locally on the file system other than the files required to run the web application.
What can cause that error to appear, seemingly at random, after a few days? And how can I fix it?
The server is Windows Server 2003, which runs IIS 6.0, and it is setup to use .Net Framework 4.0

The closest I can find online towards explaining this problem is this forum post, where the user experiencing the same error and got a replay stating:
The WinNT provider does not do well in a server environment. I am
actually suprised you don't see this with a much smaller load. I have
been able to get this with only 2 or 3 users.
and this SO comment stating
The BEST way to correctly authenticate someone is to use LogonUserAPI
as #stephbu write. All other methods described in this post will NOT
WORK 100%
where "all other methods" includes the top voted answer of using PrincipalContext.ValidateCredentials
Its sounding like PrincipalContext.ValidateCredentials isn't completely 100% reliable on Windows Server 2003 and IIS6.0, so I rewrote my authentication code to use the LogonUser WinAPI method instead.
[DllImport("advapi32.dll", SetLastError = true)]
public static extern bool LogonUser(
string lpszUsername,
string lpszDomain,
string lpszPassword,
int dwLogonType,
int dwLogonProvider,
out IntPtr phToken
);
IntPtr hToken;
if (LogonUser(username, "", password,
LOGON32_LOGON_NETWORK, LOGON32_PROVIDER_DEFAULT, out hToken))
{
...
}

Related

How to keep my application "authenticated" with an AD account? c#

I am pretty new to C#
I have been using Powershell scripts to code things like Unlocking an AD user or Enabling/Disabling an account. however, I do this with a different account, so I will log in with the admin account (Get-Credential) and storing it as '$cred' for example.
I am currently trying to do a similar thing in C# and I have found how to effectively "Authenticate"
But I am not sure how to store that Authentication, or have my app Authenticated to do things with it like Disable or Unlock an AD Account.
I have this:
public bool ADauthenticate(string username, string password)
{
bool result = false;
using (DirectoryEntry _entry = new DirectoryEntry())
{
_entry.Username = username;
_entry.Password = password;
DirectorySearcher _searcher = new DirectorySearcher(_entry);
_searcher.Filter = "(objectclass=user)";
try
{
SearchResult _sr = _searcher.FindOne();
string _name = _sr.Properties["displayname"][0].ToString();
MessageBox.Show("authenticated!");
result = true;
this.Close();
}
catch
{
MessageBox.Show("Incorrect credentials");
this.ADUsername.Text = "";
this.ADPwd.Text = "";
}
}
return result; //true = user Authenticated.
}
Which just tells me that the account is correct of course, but doesn't keep my application "authenticated", any ideas?
It's not accurate to say that your "application" was authenticated. All that was authenticated is a single network connection to your domain controller. As soon as _entry is destroyed, you lose that authentication.
If you want everything to happen using those credentials, then you have several options, ranging from easy (for you) to more difficult:
Have your users run your application under the credentials they need. Then you don't need to bother getting their username and password or setting the username and password on the DirectoryEntry object. Users can do this by:
Using Shift + right-click on the application icon and click "Run as a different user", or
Create a shortcut to: runas.exe /user:DOMAIN\username "yourapplication.exe". This will open a command window asking for the password, then start your application under those credentials.
You still ask for the username and password, but restart your application under those credentials using Process.Start().
Keep the username and password variables alive for the life of the application and pass them to every DirectoryEntry object you create in your application.
Options 1 and 2 require the computer that you're running this from is joined to the same or trusted domain as the domain you are connecting to. But since I see you're not specifying the domain name, I'm guessing that's the case.
You can do this a lot easier by using the System.DirectoryServices.AccountManagement assembly and namespace.
Add a reference to the System.DirectoryServices.AccountManagement assembly to your project, and then use this code to validate username/password against AD:
using System.DirectoryServices.AccountManagement;
// create the principal context
using (PrincipalContext ctx = new PrincipalContext(ContextType.Domain, "YourDomain"))
{
bool accountValidated = ctx.ValidateCredentials(userName, password);
// do whatever you want to do with this information
}

Why does `MemberShip.GetUser()` return null while `User.IsAuthenticated` returns true and the Username information is available in the database?

A Debian server is running a MVC4 app on Mono which uses a Membership ASPSqlProvider. The application has a method GetUserIdByName which creates a link between the Users table in the Membership database and the non-Membership Users table Users user's ID.
On my local Linux Mint system this setup does not have problem, but when the application is put on the Debian host (with the same XSP4 server and same version of Mono) and visited via the web it happens that MemberShip.GetUser() returns null. I found that out by wrapping the code in try-catch blocks and reading the exception from the website:
public int GetCurrentUserId()
{
var userName = "";
var currentUserName = "test";
if (HttpContext.Current.User.Identity.IsAuthenticated)
{
try
{
userName = HttpContext.Current.User.Identity.Name;
currentUserName = Membership.GetUser().UserName;
}
catch (Exception ex)
{
throw new Exception(ex.Message + " User is:" + userName);
}
}
var currentUserId = _repository.GetUserIdByUserName(currentUserName);
return currentUserId;
}
The exception raised reads something like
NullReferenceException User is:theusersemailaddress#gmail.com
Interestingly enough when I query the database I see that the Username is there in the Membership Users table.
A solution is to just use the users email and change the method to GetUserIdByEmail which I will do for now. Someone who provided this solution in another answer got told that this was not a real solution but a workaround, and I agree.
So the question remains: why does on the Debian machine MemberShip.GetUser() return null while User.IsAuthenticated returns true and the Username information is available in the database?
/Edit, I see now that HttpContext.Current.User.Identity.Name returns the Email column on the Debian machine, and the Username column on the Linux mint one. Why would that be?

Deleted User logged in even after deleting on the other broswer

var query = from p in AdminModelContext.Users
where p.UserName == model.UserName && p.Password == encryptPassword
&& p.IsDeleted == false
select p;
IList<Users> userList = query.ToList();
if (userList.Count() > 0)
{
FormsAuthentication.SetAuthCookie(model.UserName, model.RememberMe);
if (CheckUrl(returnUrl))
{
return Redirect(returnUrl);
}
SetRoleForUser(userList[0].RolesId);
LoggerService.Info(string.Format("Login Successful for the user : {0}",
model.UserName));
return RedirectToAction("Index", "Home");
}
I am using the following code to Login through my website. The problem that I am facing is when I logged in with a user on a specific browser and at the same time login with a different user on a different browser, and then I delete the user(logged in on the other browser). Still I am able to navigate through the pages with the deleted user logged in.
I am not finding a fair solution to put authentication logic on every page. My website is in MVC model and using Form based authentication.
Please suggest how can I put the logged in user session validation and achieve this.
None of the answers so far actually acknowledge the question.
Lets look at the control flow:
User A enters log in page, supplies valid credentials
User A is issued Ticket A.
User B enters site, supplies valid credentials.
User B is issued Ticket B.
User B then revokes User A's access CREDENTIALS.
At this point nothing happens to Ticket A. Because the Ticket is independent on the credentials. When Ticket A expires they will then be required to present their credentials and it will fail login.
So what you've noticed that kicking a live user out of your site is actually pretty hard. As you've realized the ONLY solution is to have authentication logic on EVERY request. That unfortunately is really heavy.
In the login system I built I handled this aspect by creating 2 tickets, 1 ticket that's stored in the Forms Auth ticket as normal that has a big duration, and a ticket that's stored in HttpRuntime.Cache, I set the cache expiration to 15 minutes on this ticket.
On every page request I check to see whether a user has a ticket in the cache (based off their Forms Auth ticket information), at this point if they have no ticket I do a user data refresh and poll the user database. If the user has become suspended or deleted they will be logged out at then.
Using this method I know that my site can disable a user and within 15 minutes that user will be barred from the site. If I want them immediately barred I can just cycle the app config to clear the cache and FORCE it to happen.
Normally if you have the [Authorize] attribute defined for an Controller or an Action the Authentication is checked on every post back.
The build in MembershipProvider handles all that for you. But it seems you are using your own user Database. Then you have to implement your own MembershipProvider, IPrincipal and MembershipUser and have this added to your Web.config replacing the default one.
More you'll find here how to implement your own MembershipProvider: http://msdn.microsoft.com/en-us/library/f1kyba5e.aspx
My suggestion is to create an empty MVC project and have a look at the default authentication mechanism. And if your building a new Application with a new Database, try to use the default authentication.
Your validateUser function in your own MembershipProvider could look like this.
public override bool ValidateUser(string username, string password)
{
bool isValid = false;
bool isApproved = false;
string pwd = "";
using (AdminModelContext db = new AdminModelContext())
{
var user = db.Users.FirstOrDefault(u => u.UserName == username);
if (user != null)
{
pwd = user.Password;
isApproved = user.IsApproved;
if (CheckPassword(password, pwd))
{
if (isApproved)
{
isValid = true;
user.LastLoginDate = DateTime.Now;
user.LastActivityDate = DateTime.Now;
try
{
db.SubmitChanges();
}
catch (Exception ex)
{
Console.WriteLine(ex);
}
}
}
else
{
UpdateFailureCount(username, "password");
}
}
}
return isValid;
}
I see the problem now. I dont know how this works in MVC, but by using Authenticate_Request, you can validate if the user is still valid. The business logic may also double check if the user is still valid. But as far as I know, there is no way of iterating all the open sessions and killing the requierd ones, even in that case, the authorization cookie should be double checked on Session_Start event.
Another options is adding a global invalidated_users list on the application, and then checking that user against the invalid list. This list should only contain users that are invalidated after the application has restarted.
Link for Reading All Users Session:
http://weblogs.asp.net/imranbaloch/archive/2010/04/05/reading-all-users-session.aspx

Get groups from Active Directory using C#

I am having issues getting the groups from Active Directory via System.DirectoryServices
Originally I started my application on a computer that was registered on the domain, but as it was a live domain I did not want to do any writes to AD what so ever, so I set up a machine with Windows XP as the host operating system, and installed windows server 2003 on a VM.
I've added another Ethernet port in the machine and set up a switch, the 1 Ethernet port is dedicated to the VM and the other port is used for the host.
After configuring the IP addresses to get them communicating I transferred my application onto the host machine and fired it up, but I was getting an DirectoryServicesCOMException.
With the message that the user name and password was invalid :( just to check that it was not active directory I created a 3rd virtual machine and installed Windows XP, which i added to the domain with the credentials tested in the APP, works a treat.
So I thought it must be because the machine where the application is running is not part of the domain.
Heres the block of code that was causing the issue:
public CredentialValidation(String Domain, String Username, String Password, Boolean Secure)
{
//Validate the Domain!
try
{
PrincipalContext Context = new PrincipalContext(ContextType.Domain, Domain); //Throws Exception
_IsValidDomain = true;
//Test the user login
_IsValidLogin = Context.ValidateCredentials(Username, Password);
//Check the Group Admin is within this user
//******HERE
var Results = UserPrincipal.FindByIdentity(Context, Username).GetGroups(Context);
foreach(Principal Result in Results)
{
if (Result.SamAccountName == "Domain Admins")
{
_IsAdminGroup = true;
break;
}
}
Results.Dispose();
Context.Dispose();
}
catch (PrincipalServerDownException)
{
_IsValidDomain = false;
}
}
The information in the login dialogue is being entered like so:
Domain: test.internal
Username: testaccount
Password: Password01
Hope someone can shed some light in this error.
Update:
After checking the Security Logs on the server i can see that my log in attempts was successful, but this is down to:
_IsValidLogin = Context.ValidateCredentials(Username, Password);
The line after where im checking the groups is causing the error, so the main issue is that the lines of code below are not working correctly from a machine thats not joined to the network:
var Results = UserPrincipal.FindByIdentity(Context, Username).GetGroups(Context);
According to your code snippet, you're failing when you attempt to create the PrincipalContext, before calling ValidateCredentials. At that point the thread running your code is still working under either a local identity (if you're in a web process) or the identity you signed onto your machine with (for a windows process). Either of these won't exist on the test.internal domain.
You might want to try the overload of PrincipalContext that includes the username and password in the constructor. See http://msdn.microsoft.com/en-us/library/bb341016.aspx
I used to do quite a bit of user management via C# .NET. I just dug up some methods you can try.
The following two methods will get a DirectoryEntry object for a given SAM account name. It takes a DirectoryEntry that is the root of the OU you want to start searching for the account at.
The other will give you a list of distinguished names of the groups the user is a member of. You can then use those DN's to search AD and get a DirectoryEntry object.
public List<string> GetMemberOf(DirectoryEntry de)
{
List<string> memberof = new List<string>();
foreach (object oMember in de.Properties["memberOf"])
{
memberof.Add(oMember.ToString());
}
return memberof;
}
public DirectoryEntry GetObjectBySAM(string sam, DirectoryEntry root)
{
using (DirectorySearcher searcher = new DirectorySearcher(root, string.Format("(sAMAccountName={0})", sam)))
{
SearchResult sr = searcher.FindOne();
if (!(sr == null)) return sr.GetDirectoryEntry();
else
return null;
}
}

Updating Active Directory from Web Application Error

I am receiving an error a web based application that allows corporate intranet users to update their active directory details (phone numbers, etc).
The web application is hosted on IIS6 running Windows Server 2003 (SP1). The IIS website is using NTLM Authentication and the website has integrated security enabled. The IIS application pool runs using the “Network Service” account.
The web.config contains the following elements
<LdapConfigurations server="xxx.internal" root="OU=Staff Accounts,DC=xxx,DC=internal" domain="xxx" />
<identify impersonate=”true” />
Active Directory delegation is not needed as the following C# (.NET 3.5) code should pass on the correct impersonation details (including security token) onto Active Directory.
public void UpdateData(string bus, string bus2, string fax, string home, string home2, string mob, string pager, string notes)
{
WindowsIdentity windId = (WindowsIdentity)HttpContext.Current.User.Identity;
WindowsImpersonationContext ctx = null;
try
{
ctx = windId.Impersonate();
DirectorySearcher ds = new DirectorySearcher();
DirectoryEntry de = new DirectoryEntry();
ds.Filter = m_LdapUserFilter;
// i think this is the line causing the error
de.Path = ds.FindOne().Path;
this.AssignPropertyValue(bus, ADProperties.Business, ref de);
this.AssignPropertyValue(bus2, ADProperties.Business2, ref de);
this.AssignPropertyValue(fax, ADProperties.Fax, ref de);
this.AssignPropertyValue(home, ADProperties.Home, ref de);
this.AssignPropertyValue(home2, ADProperties.Home2, ref de);
this.AssignPropertyValue(mob, ADProperties.Mobile, ref de);
this.AssignPropertyValue(pager, ADProperties.Pager, ref de);
this.AssignPropertyValue(notes, ADProperties.Notes, ref de);
// this may also be causing the error?
de.CommitChanges();
}
finally
{
if (ctx != null)
{
ctx.Undo();
}
}
}
private void AssignPropertyValue(string number, string propertyName, ref DirectoryEntry de)
{
if (number.Length == 0 && de.Properties[propertyName].Value != null)
{
de.Properties[propertyName].Remove(de.Properties[propertyName].Value);
}
else if (number.Length != 0)
{
de.Properties[propertyName].Value = number;
}
}
User details can be retrieved from Active Directory without a problem however the issue arises when updating the users AD details. The following exception message is displayed.
System.Runtime.InteropServices.COMException (0x80072020): An operations error occurred.
at System.DirectoryServices.DirectoryEntry.Bind(Boolean throwIfFail)
at System.DirectoryServices.DirectoryEntry.Bind()
at System.DirectoryServices.DirectoryEntry.get_AdsObject()
at System.DirectoryServices.DirectorySearcher.FindAll(Boolean findMoreThanOne)
at System.DirectoryServices.DirectorySearcher.FindOne()
at xxx.UpdateData(String bus, String bus2, String fax, String home, String home2, String mob, String pager, String notes)
at xxx._Default.btnUpdate_Click(Object sender, EventArgs e)
The code works fine in our development domain but not in our production domain. Can anyone please assist in helping resolving this problem?
This is more than likely a permissions problem - there are numerous articles regards impersonation and delegation and the vagaries thereof here: http://support.microsoft.com/default.aspx?scid=kb;en-us;329986 and here: http://support.microsoft.com/default.aspx?scid=kb;en-us;810572.
It sounds like you might have a duplicate SPN issue?
This is why I think it might be a problem:
It works in your dev enviroment (assuming it is also using network service, and on the same domain)
You have impersonate on, in your web config.
When there is a duplicate SPN, it invalidates the security token, so even though you have created it correctly in code, AD does not "trust" that server to impersonate, so the server that receives the request to make a change to AD (on of your DC's) receives the request but then discards it because Delagation permission has not been applied on the machine account in AD, or SPN issue (either duplicate or incorrect machine name / domain name)
Or at least in my expereince that is 9 out of 10 times the problem.
I guess the problem is that it works on the development environment because when you're launching your webapp there, you run it with your personal account which probably has the rights to write to AD.
On the production environment, you have to assure that the process running your webapp (Network Service Account) has also the rights to update the AD. It sounds to me like that could be the problem since I had a similar issue once.
The problem wasn't with the code but how the server was setup on the domain. For some reason the network administrator did not select the "Trust Computer for Delegation" option in active directory.
Happily the problem was not a "double-hop" issue :)

Categories