I have a WCF service, which works if I use one login, but throws the following error if I try logging in with any other login. Strangely enough, if I change the password to the working login, the new password doesn't work but the old one still does. It's almost like it is caching something.
The error I get is this:
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
The code that causes the error is this:
public UserModel Login(string username, string password)
{
if (username == null || password == null)
return null;
using (var pContext = new PrincipalContext(ContextType.Machine))
{
if (pContext.ValidateCredentials(username, password))
{
using (var context = new MyEntities())
{
// I can tell from a SQL trace that this piece never gets hit
var user = (from u in context.Users
where u.LoginName.ToUpper() == username.ToUpper()
&& u.IsActive == true
select u).FirstOrDefault();
if (user == null)
return null;
var userModel = Mapper.Map<User, UserModel>(user);
userModel.Token = Guid.NewGuid();
userModel.LastActivity = DateTime.Now;
authenticatedUsers.Add(userModel);
sessionTimer.Start();
return userModel;
}
}
}
return null;
}
I see a related question here, which suggests the problem is with the PrincipalContext, but no answer
Update
Got it working..... I restarted our production server because we needed to have this working for someone important within the next hour, and I thought since it that previous link suggested that a reboot would get a single login in that I would just reboot and login with the login needed to get it working for now, and after rebooting everything works absolutely perfectly. I spent most of yesterday, staying late, and all of this morning trying to figure this out. We're not supposed to reboot our web server, but it was important to get this working so I did it anyways, and now everything works the way it should.
I would still like to know what its problem was though. My best guess is that something caused the PrincipalContext to not dispose correctly, which was preventing me from logging in with any other set of credentials.
Restarting the server fixed the issue, although I'd still love to know what the problem was.
My best guess is that something caused the PrincipalContext to not dispose correctly, which was preventing me from logging in with any other set of credentials.
Related
We have a mvc application that is using Active Directory to authenticate our users. We are leveraging System.DirectoryServices and using the PricipalContext to authenticate:
_principalContext.ValidateCredentials(userName, pass, ContextOptions.SimpleBind);
However this method only returns a bool and we want to return better messages or even redirect the user to a password reset screen for instances like:
The user is locked out of their account.
The users password is expired.
The user needs to change their password at next login.
So if the user fails to login we call NetValidatePasswordPolicy to see why the user was not able to log in. This seemed to work well but we realized that this method was only returning NET_API_STATUS.NERR_PasswordMustChange no matter what the state of the Active Directory user was.
The only example I have found with this same problem comes from a Sublime Speech plugin here. The code I am using is as follows:
var outputPointer = IntPtr.Zero;
var inputArgs = new NET_VALIDATE_PASSWORD_CHANGE_INPUT_ARG { PasswordMatched = false, UserAccountName = username };
inputArgs.ClearPassword = Marshal.StringToBSTR(password);
var inputPointer = IntPtr.Zero;
inputPointer = Marshal.AllocHGlobal(Marshal.SizeOf(inputArgs));
Marshal.StructureToPtr(inputArgs, inputPointer, false);
using (new ComImpersonator(adImpersonatingUserName, adImpersonatingDomainName, adImpersonatingPassword))
{
var status = NetValidatePasswordPolicy(serverName, IntPtr.Zero, NET_VALIDATE_PASSWORD_TYPE.NetValidateAuthentication, inputPointer, ref outputPointer);
if (status == NET_API_STATUS.NERR_Success)
{
var outputArgs = (NET_VALIDATE_OUTPUT_ARG)Marshal.PtrToStructure(outputPointer, typeof(NET_VALIDATE_OUTPUT_ARG));
return outputArgs.ValidationStatus;
}
else
{
//fail
}
}
The code always succeeds so why is the value of outputArgs.ValidationStatus the same result every time regardless of the state of the Active Directory user?
I will break the answer to this question into three different sections:
The Current Problem With Your Methodology
The Issues With Recommended Solutions both Online, and in this Thread
The Solution
The current problem with your methodology.
NetValidatePasswordPolicy requires its InputArgs parameter to take in a pointer to a structure, and the structure you pass in depend on the ValidationType your're passing in. In this case, you are passing NET_VALIDATE_PASSWORD_TYPE.NetValidateAuthentication, which requires an InputArgs of NET_VALIDATE_AUTHENTICATION_INPUT_ARG but you're passing in a pointer to NET_VALIDATE_PASSWORD_CHANGE_INPUT_ARG.
Furthermore, you are attempting to assign a "currentPassword' type of value to the NET_VALIDATE_PASSWORD_CHANGE_INPUT_ARG structure.
However, there's a bigger fundamental proble to the use of NetValidatePasswordPolicy and that is that you are trying to use this function to validate passwords in Active Directory, but this is not what it is used for. NetValidatePasswordPolicy is used to allow applications to validate against a authentication database provided by the application.
There's more information about NetValidatePasswordPolicy here.
The issues with recommended solutions both online, and in this thread
Various articles online recommend using the LogonUser function found in AdvApi32.dll but this implementation carries its own set of issues:
The first is that LogonUser validates against a local cache, and that means that you will not get immediate accurate information about the account, unless you use the "Network" mode.
The second is that using LogonUser on a Web application, in my opinion is a bit hacky, as it is designed for desktop applications running on client machines. However, considering the limitations provided Microsoft if LogonUser gives desired results, I don't see why it shouldn't be used - barring the caching issues.
Another issue with LogonUser is that how well it works for your use case depends on how your server is configured, for example: There are some particular permissions that need to be enabled on the domain you're authenticating against that need to be in place for 'Network' logon type to work.
More information about LogonUser here.
Also, GetLastError() should not be used, GetLastWin32Error() should be used instead, as it is not safe to use GetLastError().
More information about GetLastWin32Error() here.
The solution.
In order to get an accurate error code from Active Directory, without any caching issues and straight from directory services, this is what needs to be done: rely on COMException coming back from AD when there's an issue with the account, because ultimately, errors is what you're looking for.
First, here's how you trigger an error from Active Directory on authentication of a current user name and a password:
public LdapBindAuthenticationErrors AuthenticateUser(string domain, string username, string password, string ouString)
{
// The path (ouString) should not include the user in the directory, otherwise this will always return true
DirectoryEntry entry = new DirectoryEntry(ouString, username, password);
try
{
// Bind to the native object, this forces authentication.
var obj = entry.NativeObject;
var search = new DirectorySearcher(entry) { Filter = string.Format("({0}={1})", ActiveDirectoryStringConstants.SamAccountName, username) };
search.PropertiesToLoad.Add("cn");
SearchResult result = search.FindOne();
if (result != null)
{
return LdapBindAuthenticationErrors.OK;
}
}
catch (DirectoryServicesCOMException c)
{
LdapBindAuthenticationErrors ldapBindAuthenticationError = -1;
// These LDAP bind error codes are found in the "data" piece (string) of the extended error message we are evaluating, so we use regex to pull that string
if (Regex.Match(c.ExtendedErrorMessage, #" data (?<ldapBindAuthenticationError>[a-f0-9]+),").Success)
{
string errorHexadecimal = match.Groups["ldapBindAuthenticationError"].Value;
ldapBindAuthenticationError = (LdapBindAuthenticationErrors)Convert.ToInt32(errorHexadecimal , 16);
return ldapBindAuthenticationError;
}
catch (Exception e)
{
throw;
}
}
return LdapBindAuthenticationErrors.ERROR_LOGON_FAILURE;
}
And these are your "LdapBindAuthenticationErrors", you can find more in MSDN, here.
internal enum LdapBindAuthenticationErrors
{
OK = 0
ERROR_INVALID_PASSWORD = 0x56,
ERROR_PASSWORD_RESTRICTION = 0x52D,
ERROR_LOGON_FAILURE = 0x52e,
ERROR_ACCOUNT_RESTRICTION = 0x52f,
ERROR_INVALID_LOGON_HOURS = 0x530,
ERROR_PASSWORD_EXPIRED = 0x532,
ERROR_ACCOUNT_DISABLED = 0x533,
ERROR_ACCOUNT_EXPIRED = 0x701,
ERROR_PASSWORD_MUST_CHANGE = 0x773,
ERROR_ACCOUNT_LOCKED_OUT = 0x775
}
Then you can use the return type of this Enum and do what you need with it in your controller. The important thing to note, is that you're looking for the "data" piece of the string in the "Extended Error Message" of your COMException because this contains the almighty error code you are hunting for.
Good luck, and I hope this helps. I tested it, and it works great for me.
Determined this can't be a network issue. I'm having this issue in debug (VS2012 / .Net 4.5 / IIS Express 8.0)
Code:
bool rtn2 = HttpContext.Current.User.IsInRole("MyDomain\\Domain Users");
Eventually returns true. But, can take several minutes.
var test = HttpContext.Current;
var test2 = HttpContext.Current.User;
var test3 = HttpContext.Current.User.Identity;
...all extremely fast.
var test = HttpContext.Current.User.IsInRole("MyDomain\\Domain Users");
var test2 = HttpContext.Current.User.IsInRole("MyDomain\\Domain Users");
First call takes several minutes, the second is instant. If I change the second to look for some other group (assuming the first was cached), it is still instant.
I thought maybe i'm having network issues (I connect to the domain and debug over VPN.) However, if I create a new VS2012 web project and put that code in the startup page, it's instant. I can also search Active Directory from my machine and pull up the Domain Users group and see all people in it pretty much instantly (there are over 10 thousand users) - no problem. So, this must be project / config based issue?
Going out of my mind trying to figure this out. Some info:
Tried re-installing IIS Express
I've tried rebooting
I've tried in a new tester web project - works instantly
Problem seems to be machine specific. Any assistance or even just recommendations for additional trouble-shooting steps would be appreciated.
Try using the System.DirectoryServices.AccountManagement namespace instead.
public static bool IsUserGroupMember(string userName, string groupName)
{
using (PrincipalContext context = new PrincipalContext(ContextType.Domain))
using (UserPrincipal user = UserPrincipal.FindByIdentity(context, userName))
using (PrincipalSearchResult<Principal> groups = user.GetAuthorizationGroups())
{
return groups.OfType<GroupPrincipal>().Any(g => g.Name.Equals(groupName, StringComparison.OrdinalIgnoreCase));
}
}
I had the same problem. IsInRole was taking forever to finish on a production server. The following code worked instead. Saw it somewhere, unfortunately can't remember the source.
// Is in AD Group?
private static bool IsInADGroup(String inGroup)
{
foreach (System.Security.Principal.IdentityReference group in
System.Web.HttpContext.Current.Request.LogonUserIdentity.Groups)
{
String sGroup = (group.Translate(typeof(System.Security.Principal.NTAccount)).ToString());
if (sGroup.Equals(inGroup))
return true;
}
return false;
}
History
We have an elaborate system which ties into DNN on multiple levels for custom user information. Our users are governed under a completely different database than DNN. Because of this, we have special requirements and have created our own membership provider to our database and registration form for our specific needs.
The Problem
In the past, we have used our membership provider and registration form with no problems. Recently, we have encountered a strange situation where the username is mangled after someone attempts to register a new account. It is mangled in such a way that the username becomes the user's Region followed by a hyphen, followed by the user's email address in full. As an example, if a user fills out the form with the following:
username: user
region: CA
email: me#example.com
Before UserController.CreateUser(user) is called, the userInfo.Username equals "user". However, after the call completes, it will be "CA-me#example.com" and the created record in the User table will reflect this change.
Using my custom membership provider (which calls the default AspNetMembershipProvider) verifies that the issue seems to be with AspNetMembershipProvider's CreateUser(ref userInfo); Before this call, the username is "user", but after this call completes it becomes "CA-me#example.com". The result of the call is Success and the user is created in the database. So it doesn't appear to be a failure that is causing the issue.
Settings
I'm not sure if there are settings on DNN which may cause this problem. In my development environment, this entire thing is a non-issue. It only occurs in the live and a secondary internal test environment. Since I'm not able to reproduce this bug in my debug environment, I've considered this might be a site configuration issue. The only option I've found on Site Settings is to make the username the same as the email address, but this is disabled.
I'm going to post my membeship provider code for updating the DNN record, and I'll post the code which creates the user in my register module. I don't know if anyone might need additional code, but I'm willing to provide more where necessary.
Membership Provider
/// <summary>
/// Updates the user in the DNN database.
/// </summary>
/// <param name="userInfo"></param>
private void UpdateDnn(UserInfo userInfo)
{
// _defaultProvider is an instance of AspNetMembershipProvider
if (_defaultProvider.GetUser(userInfo.PortalID, userInfo.UserID) != null)
_defaultProvider.UpdateUser(userInfo);
else
_defaultProvider.CreateUser(ref userInfo);
}
Register Module
public DotNetNuke.Security.Membership.UserCreateStatus Register(Inputs.RegistrationInput input)
{
var userInfo = new UserInfo();
input.Fill(portalId, userInfo); // This copies the user input into the userInfo object.
return UserController.CreateUser(ref userInfo);
}
RegistrationInput.Fill()
public void Fill(int portalId, UserInfo userInfo)
{
if (userInfo == null) throw new ArgumentNullException("userInfo");
userInfo.PortalID = portalId;
userInfo.FirstName = FirstName;
userInfo.LastName = LastName;
userInfo.Username = UserName;
userInfo.DisplayName = DisplayName;
userInfo.Email = Email;
userInfo.Username = UserName;
userInfo.Membership.Password = Password;
userInfo.Profile.Street = Street;
userInfo.Profile.Unit = SuiteApt;
userInfo.Profile.Region = State;
userInfo.Profile.City = City;
userInfo.Profile.PostalCode = Zip;
userInfo.Profile.Telephone = PrimaryPhone;
}
Update
I was digging around in DDN's core for a work around and stumbled upon this (AspNetMembershipProvider.cs:870):
// Check that the OAuth service currently being used for login is the same as was previously used (this should always be true if user authenticated to userid)
if (authUser == null || authUser.AuthenticationType.Equals(service, StringComparison.OrdinalIgnoreCase))
{
isOAuthUser = true;
//DNN-4133 Change username to email address to ensure multiple users with the same email prefix, but different email domains can authenticate
user.Username = service + "-" + user.Email;
}
else
{
createStatus = UserCreateStatus.DuplicateEmail;
}
The line user.Username = service + "-" + user.Email is in the exact format of the username that I am receiving after I call UserController.CreateUser(). The question now is: is this an error in my code or their code? I'm going to keep digging into this, and if nobody answers, I'll try to post an answer to this problem after I understand what is happening here. Furthermore, if I find this to be a bug in their code, I'll post a bug report on DNN's bug tracker and link back to the bug page here.
I finally found the problem. It turns out that the issue is with a line of code above the posted code in the membership provider in the same function:
string service = HttpContext.Current.Request.Params["state"];
The problem with this line is that I pass state via POST as the user's state of residency. DNN seems to consider it the service state. The solution to this is to change the POST input parameter name from state to something else - preferably region as DNN recognizes states as regions. An easy fix, all-in-all, but it took me quite a while to find it. I think DNN should have allowed this parameter to be changed via the state of the membership object or parameter rather than grabbing it directly from the request, but there's not much that can be done on our end in that regard (modifying the core source is strongly discouraged).
I have this piece of code in a program, to query what groups a Windows-domain user belongs to.
public void GetGroupNames(string userName, List<string> result)
{
using (PrincipalContext pc = new PrincipalContext(ContextType.Domain))
{
UserPrincipal uPrincipal = UserPrincipal.FindByIdentity(pc, userName);
if (uPrincipal != null)
{
PrincipalSearchResult<Principal> srcList = uPrincipal.GetGroups();
foreach (Principal item in srcList)
{
result.Add(item.ToString());
}
}
}
}
When I just implemented it and was debugging it,
UserPrincipal uPrincipal = UserPrincipal.FindByIdentity(pc, userName);
always got null.
I then had to close visual studio to do something else. When I came back, opened up visual studio, this code just worked. A few days ago, there was a network problem in the organisation, I did not switch off my PC during that period. After the network went back to normal, I could connect to internet OK, I could remote desktop to servers etc, which proves that Active Directory authentication was done all right, but the above piece of code failed to find UserPrinical for a given name, e.g. my own. I then reboot the PC, the code worked fine. I am quite puzzled regarding this matter. Is anyone able to provide a good explanation for this??
UserPrincipal has some known bugs with in cross domain scenarios. When it happens again, look and see if you can resolve groups from your machine. I have also encountered problems when unresolvable SIDs where group members.
Update
Thanks to a comment by #IvanL, it turns out that the problem is Google specific. I have since tried other providers and for those everything works as expected. Google just doesn't seem to send claims information. Haven't yet been able to figure out why or what I need to differently to get Google to send it.
A wild stab in the dark says it may be related to the realm being defaulted to http://:/ as I have seen an answer by Andrew Arnott that Google changes the claimed identifier for the same account based on the realm passed with the authentication request.
Another possibly important tidbit of information: unlike many of the examples that can be found around the web for using dotnetopenauth, I am not using a "simple" textbox and composing the openIdIdentifier myself, but I am using the openID selector and that is providing the openIdIdentifier passed to the ValidateAtOpenIdProvider. (As per the Adding OpenID authentication to your ASP.NET MVC 4 application article.)
Question is: why is IAuthenticationResponse.GetExtension() always returning null when using Google as the openId provider, when otherwise all relevant gotcha's with regard to Google (Email requested as required, AXFetchAsSregTransform, etc) have been addressed?
Original
I am struggling with getting DotNetOpenAuth to parse the response returned from the provider. Followed the instructions of Adding OpenID authentication to your ASP.NET MVC 4 application up to the point where the login should be working and a login result in a return to the home page with the user's name (nick name) displayed at the top right. (That is up to "The user should at this point see the following:" just over half way down the article).
I am using Visual Studio Web Developer 2010 Express with C#. DotNetOpenAuth version is 4.0.3.12153 (according to the packages.config, 4.0.3.12163 according to Windows Explorer).
My web.config was modified following the instructions in Activating AXFetchAsSregTransform which was the solution for DotNetOpenId - Open Id get some data
Unfortunately it wasn't enough to get it working for me.
The openid-selector is working fine and resulting in a correct selection of the openid provider. The authentication request is created as follows:
public IAuthenticationRequest ValidateAtOpenIdProvider(string openIdIdentifier)
{
IAuthenticationRequest openIdRequest = openId.CreateRequest(Identifier.Parse(openIdIdentifier));
var fields = new ClaimsRequest()
{
Email = DemandLevel.Require,
FullName = DemandLevel.Require,
Nickname = DemandLevel.Require
};
openIdRequest.AddExtension(fields);
return openIdRequest;
}
This all works. I can login and authorize the page to receive my information, which then results in a call to GetUser:
public OpenIdUser GetUser()
{
OpenIdUser user = null;
IAuthenticationResponse openIdResponse = openId.GetResponse();
if (openIdResponse.IsSuccessful())
{
user = ResponseIntoUser(openIdResponse);
}
return user;
}
openIdResponse.IsSuccessful is implemented as an extension method (see linked article):
return response != null && response.Status == AuthenticationStatus.Authenticated;
and always is successful as the ResponseIntoUser method is entered:
private OpenIdUser ResponseIntoUser(IAuthenticationResponse response)
{
OpenIdUser user = null;
var claimResponseUntrusted = response.GetUntrustedExtension<ClaimsResponse>();
var claimResponse = response.GetExtension<ClaimsResponse>();
// For this to work with the newer/est version of DotNetOpenAuth, make sure web.config
// file contains required settings. See link for more details.
// http://www.dotnetopenauth.net/developers/help/the-axfetchassregtransform-behavior/
if (claimResponse != null)
{
user = new OpenIdUser(claimResponse, response.ClaimedIdentifier);
}
else if (claimResponseUntrusted != null)
{
user = new OpenIdUser(claimResponseUntrusted, response.ClaimedIdentifier);
}
else
{
user = new OpenIdUser("ikke#gmail.com;ikke van ikkenstein;ikke nick;ikkeclaimedid");
}
return user;
}
My version above only differs from the code in the linked article by my addition of the final else block to ensure that I always get the home page with a user name and a logoff link displayed (which helps when trying to do this several times in succession).
I have tried both Google and Yahoo. Both authenticate fine, both return an identity assertion as logged by the WebDev server. However, GetUntrustedExtenstion and GetExtension always return null. I always get to see "ikke nick" from the last else, never the name I actually used to authenticate.
I am at a loss on how to continue to try and get this to work. It probably is some oversight on my part (I am an experienced developer but just started dipping my toes in C# and web front-end development), and I can't see it.
Any and all suggestions on how to proceed / debug this are very much welcome.
Are you using Google as OpenId provider to test your solution against? Because Google has/had the habit of including the Claims only the first time you authenticate the application. So perhaps try using a fresh google account and see if that works?
Sorry for the slow response, doing a big migration at a client this week :-) Glad that this little comment resolved your issue.