C# LDAP performance - c#

Where I work, we have two modes of authentication:
CAS (http://www.jasig.org/cas)
LDAP
CAS is the primary method, but it is often unreliable at peak traffic times and so we have been using LDAP as a fallback mode for when we notice that CAS is down. Previously, we were using PHP for doing our LDAP fallback and got reasonable performance. There wasn't a noticeable delay during login other than the expected network lag times. A login took probably ~250-500ms to complete using LDAP.
Now, we are making a new system and have chosen ASP.NET MVC4 as the platform rather than PHP and I am tasked with trying to get this fallback working again. I have been pulling my hair out for about 6 hours now trying different things over and over again, getting the same result (perhaps I am insane). I have finally managed to connect to LDAP, authenticate the user, and get their attributes from LDAP. However, the query consistently takes 4.5 seconds to complete no matter what method I try.
This is very surprising to me seeing as the PHP version was able to do nearly the same thing in 1/8th the time and it would seem that the .NET framework has excellent support for LDAP/ActiveDirectory. Am I doing something incredibly horribly wrong?
Here are the guts of my function as it stands now (this one is the latest iteration that manages to do everything in one 4.5 second query):
public Models.CASAttributes Authenticate(string username, string pwd)
{
string uid = string.Format("uid={0},ou=People,o=byu.edu", username);
LdapDirectoryIdentifier identifier = new LdapDirectoryIdentifier("ldap.byu.edu", 636, false, false);
try
{
using (LdapConnection connection = new LdapConnection(identifier))
{
connection.Credential = new NetworkCredential(uid, pwd);
connection.AuthType = AuthType.Basic;
connection.SessionOptions.SecureSocketLayer = true;
connection.SessionOptions.ProtocolVersion = 3;
string filter = "(uid=" + username + ")";
SearchRequest request = new SearchRequest("ou=People,o=byu.edu", filter, SearchScope.Subtree);
Stopwatch sw = Stopwatch.StartNew();
SearchResponse response = connection.SendRequest(request) as SearchResponse;
sw.Stop();
Debug.WriteLine(sw.ElapsedMilliseconds);
foreach (SearchResultEntry entry in response.Entries)
{
Debug.WriteLine(entry.DistinguishedName);
foreach (System.Collections.DictionaryEntry attribute in entry.Attributes)
{
Debug.WriteLine(attribute.Key + " " + attribute.Value.GetType().ToString());
}
Debug.WriteLine("");
}
}
}
catch
{
Debugger.Break();
}
Debugger.Break();
return null; //debug
}
The PHP version of this follows this sequence:
Bind anonymously and look up the user information using a basedn and cn
Bind again using the username and password of the user to see if they are authentic
It does two binds (connects?) in 1/8th the time it takes the .NET version to do one! Its this sort of thing that makes me thing I am missing something.
I have tried methods based on the following sites:
http://roadha.us/2013/04/ldap-authentication-with-c-sharp/ - Required 2 queries to do what I wanted and was too slow. I went through probably 6 different tries of doing it differently (varying the authentication & connection settings, etc).
http://web.byu.edu/docs/ldap-authentication-0 - One PHP version, but has a small snippet about .NET at the bottom. I needed to get the profile as well and they weren't exactly descriptive.
System.DirectoryServices is slow? - Current version
EDIT:
Using wireshark, I saw that the following requests are made:
bindRequest passing along my uid (delta 0.7ms)
bindResponse success (delta 2ms)
searchRequest "ou=People,o=byu.edu" wholdSubtree (delta 0.2ms)
searchResEntry "uid=my uid,ou=People,o=byu.edu" | searchResDone success 1 result (delta 10.8ms)
unbindRequest (delta 55.7ms)
Clearly, the overhead is coming from .NET and not from the requests. These don't add up to 4.5 seconds in any way, shape, or form.

ldap.byu.edu sure looks like a fully qualified DNS host name. You should change your LdapDirectoryIdentifier constructor to new LdapDirectoryIdentifier("ldap.byu.edu", 636, true, false).

I think you're definitely on the right track using System.DirectoryServices for this, so you may just need to tweak your search request a bit.
You're only looking to get one result back here, correct? Set your size accordingly :
request.SizeLimit = 1;
This is a tricky one, but make also sure you're suppressing referral binds as well. You'll want to set this before you call connection.SendRequest(request) :
//Setting the DomainScope will suppress referral binds from occurring during the search
SearchOptionsControl SuppressReferrals = new SearchOptionsControl(SearchOption.DomainScope);
request.Controls.Add(SuppressReferrals);

Related

Calling NetValidatePasswordPolicy from C# always returns Password Must Change

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.

Authenticating to AD using sAMAccountName format using PrincipleContext

I am implementing AD authentication for an offline application.
My original code did the following:
var validAuth = false;
using (var context = new System.DirectoryServices.AccountManagement.PrincipalContext(System.DirectoryServices.AccountManagement.ContextType.Domain))
{
validAuth = context.ValidateCredentials(_viewModel.Username, txtPassword.Password);
}
However, during testing it was noticed that this caused account lockouts in half the number of attempts of the AD Group Policy - so say the policy was set to 4 attempts before lockout, the user would be locked out in 2.
I googled and found this article on MSDN and the TL;DR is this:
It sounds that bad password count increase by 2 if you use UPN format (Domain#sAMAccountName), however, the count increase 1 every time if you use sAMAccountName format (Domain\sAMAccountName).
In light of this I changed my code to this:
var validAuth = false;
using (var context = new System.DirectoryServices.AccountManagement.PrincipalContext(System.DirectoryServices.AccountManagement.ContextType.Domain))
{
var usernameToAuth = string.Format("{0}\\{1}", Environment.UserDomainName, _viewModel.Username);
validAuth = context.ValidateCredentials(usernameToAuth, txtPassword.Password);
}
But this now fails to authenticate regardless of input. If I change it to use the old style UPN format of user#domain it authenticates fine - but obviously that is using up two authentication requests.
The MSDN post says to use the sAMAccountName format as a work around but I am struggling to work out how to do this. My original code also didn't explicitly use the old UPN format - I just passed the User Name directly to the ValidateCredentials method (no # symbol anywhere to be seen) so does this method use the old UPN method first?
Any advice please - I don't particularly want to half the bad log on attempts our users can have.
I used the domain specification in the PrincipalContext constructor, specified in this post, like that:
public static bool IsAuthenticated(string username_, string password_)
{
using (var pc = new PrincipalContext(ContextType.Domain, DomainManager.DomainName))
return pc.ValidateCredentials(username_, password_);
}
In my case, I use the System.DirectoryServices.ActiveDirectory.Domain and System.DirectoryServices.ActiveDirectory.DomainController to get this DomainManager.DomainName values.

HttpContext.Current.User.IsInRole("Domain\\Domain Users") takes forever

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;
}

IAuthenticationResponse.GetExtension<ClaimsResponse>() always returning null

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.

Can I get only the new Facebook comments since the last time I checked instead of all comments every time?

Right now I have an app that allows a user to schedule/post a Facebook post and then monitor the likes/comments. One of the problems I foresee is that currently I am pulling every single comment/like whether it's been processed or not. What I would like to do instead is be able to say 'Give me all the NEW comments since XYZdate/XYZcomment.' Is this currently possible?
var accessToken = existingUserNode.Attributes["accessToken"].Value;
var facebookAPIMgr = new FacebookWrapper.FacebookAPIManager();
var msg = new FacebookWrapper.FacebookMessage()
{
AccessToken = accessToken,
FacebookMessageId = facebookPost.FacebookMessageId
};
//Get Facebook Message Comments
// Need to find a way to limit this to only new comments/likes
var comments = facebookAPIMgr.RetrieveComments(msg);
You can do time-based pagination as part of your graph API query. If you keep a unix timestamp of when you polled things last, you can simply do https://graph.facebook.com/{whatever}?since={last run}.
This worked when I was working heavily with the Graph API earlier this year, and is still around on the documentation, but considering how much Facebook loves to change stuff without telling anyone you may still encounter problems. So just a warning, YMMV.

Categories

Resources