Authenticating to AD using sAMAccountName format using PrincipleContext - c#

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.

Related

Query by example for Locked Out Accounts in C#

I would like to use query by example to show me all the locked out accounts in my OU.
I was able to successfully do something similar with Enabled accounts and also Smart Card Logon Required accounts.
For some reason, userPrincipal.IsAccountLockedOut() seems to function different than userPrincipal.Enabled
Basically , it seems to be a method rather than a variable.
I searched online and couldn't find any relevant answers or documentation specific to this use case.
Here is my code, currently:
bool enabled = true;
bool locked = false;
string firstName = "John";
PrincipalContext ctx = new PrincipalContext(ContextType.Domain);
UserPrincipal up = new UserPrincipal(ctx);
up.Enabled = enabled;
up.GivenName = firstName;
But, up.IsAccountLockedOut() = locked; doesn't work. Neither does locked = up.IsAccountLockedOut()
IsAccountLockedOut() functions differently than Enabled since those are two different things:
An account is locked out by too many wrong password attempts. This is to prevent brute force attempts to guess a password. Accounts are usually automatically unlocked after a period of time. The number of wrong attempts that triggers a lockout and the time before automatic unlocking is configurable by the domain admin.
Disabling an account (Enabled == false) is when an administrator has specifically disabled the account. No one will be able to authenticate with a disabled account, even if they know the right password.
To find locked out accounts, you want to look at the lockoutTime attribute. It stores the time the account was locked out. A value of 0 means it's not locked. So you want to look for accounts where the value is greater than 0. This would be the LDAP query:
(&(objectCategory=person)(objectClass=user)(lockoutTime>=1))
You have to use >=1 since the LDAP spec doesn't actually support >.
I assume you're asking this question since you're trying to search with PrincipalSearcher, which limits you to searching based on properties that UserPrincipal exposes to you. Since the lockoutTime attribute is not exposed by UserPrincipal, you can't do it that way. You'll have to use DirectorySearcher directly (which is what PrincipalSearcher uses behind the scenes anyway). Here is an example that would output the username and the time that the lockout occurred:
var searcher = new DirectorySearcher() {
Filter = "(&(objectCategory=person)(objectClass=user)(lockoutTime>=1))",
PageSize = 1000, //make sure we get more than one page, if needed
PropertiesToLoad = { "sAMAccountName", "lockoutTime" } //which atrributes you want to use
};
using (var results = searcher.FindAll()) {
foreach (SearchResult result in results) {
var username = (string) result.Properties["sAMAccountName"][0];
var lockoutTime = DateTime.FromFileTime((long) result.Properties["lockoutTime"][0]);
Console.WriteLine($"{username} was locked out at {lockoutTime}");
}
}
Personally, I've stopped using UserPrincipal/PrincipalSearcher altogether because performance is always worse (sometimes it's not noticeable, other times it absolutely is), and there are times like this when you can't use it anyway. I wrote an article about getting the best performance when talking to AD, if you're interested: Active Directory: Better performance

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.

C# Ask for Domain Admin credential and use them to perform some task

I need some help with examples how to use Credential of a current user running application.
So in windows 7 you can run application using user loged in by simply running application or you can use "Run as a different User" option and run it as another user.
In my Active Directory I have 2 account Domain User and one with Domain Admin rights. I'm login Windows as a Domain User and when I need I'm using "Run as a different User" to launch some task as a Domain Admin.
So the task is to get my Credential and use it to perform some task, lets say rename active directory user name.
Best way to do this as I can see is to ask user running application to enter Domain Admin credential on then start application and use them for various task. Of course I can easily run application with "Run as a different User" but I still need to get this credential and use them.
I've searched through the web and I can't find this, all i could find is using credential for a web auth.
If you can show me some examples how to:
1) Ask user for a Admin user credential ( i can leave without this )
2) Get and use credentials of a user running application
I don't want to know password I know I can't. Don't really want to add to a WPF form password box I prefer to use windows API to handle this i've already entered user name and password using "Run as a different User".
PS: I sorry if this topic exists :( I guess I'm bad at creating correct search requests.
ADDED: to be more clear what I need. In powershell it will look like this:
# This Asks user to enter credentials
$cred = Get-Credential;
# this checks if I have rights to use them.
Get-ADDomain “DOMAIN” –Server “Domain.com” –Credential $cred;
Of course it's simplified as hell though the point is that I can use credentials user entered when ever it's needed.
The equivalent C# to your Get-ADDomain is quite simple, it is just
public void PerformSomeActionAsAdmin(string adminUsername, string adminPassword)
{
//Null causes the constructor to connect to the current domain the machine is on.
// |
// V
using (PrincipalContext ctx = new PrincipalContext(ContextType.Domain, null, adminUsername, adminPassword))
{
//do something here with ctx, the operations will be performed as whoever's username and password you passed in.
}
}
if you don't want to connect to the current domain and instead want to connect to Domain.com then replace the null with the appropriate string.
EDIT: if you want to use secure strings you can't use System.DirectoryServices.AccountManagement.PrincipalContext, you will need to go with the lower level calls in System.DirectoryServices.Protocols. Doing this process is quite complex, here is a link to the MSDN article "Introduction to System.DirectoryServices.Protocols (S.DS.P)" explaining how to use it. It is a big complex read and honestly I don't think it is worth it to be able to use encrypted strings.
public void PerformSomeActionAsAdmin(NetworkCredential adminCredential)
{
using(LdapConnection connection = new LdapConnection("fabrikam.com", adminCredential))
{
// MAGIC
}
}
Do you want to check if the current user is a doman admin? start by looking at his code, it should help you get started identifying what AD groups the current user is in. This will give you a list of strings that are each group's name the current user belongs to. Then you can check that list against whatever AD group you are trying to check for. Replace YourDomain with your domain name:
WindowsIdentity wi = WindowIdentity.GetCurrent();
List<string> result = new List<string>();
foreach (IdentityReference group in wi.Groups)
{
result.Add(group.Translate(typeof(NTAccount)).ToString().Replace("YourDomain\\", String.Empty));
}
Since i'm not quite sure what you're trying to do, this also might be helpful. You'd have to get the user name and password from a textobx, password box etc. This could be used for an "override" to use, for example, a manager's credentials etc. to do something the current user wasn't allowed to do because of AD group membership etc.
using (PrincipalContext pc = new PrincipalContext(ContextType.Domain, "YourDomain"))
{
if (UserName.Contains("YourDomain\\"))
{
UserName = UserName.Replace("YourDomain\\", String.Empty);
}
//validate the credentials
bool IsValid = pc.ValidateCredentials(UserName, Password);
}

Active Directory Account Password Expiration Date with Fine-Grain Password Policy

I'm having no issue getting the password expiration date in a pre-Windows 2008 domain envrionment. I'm able to get the default domain policy and get the password expiration date.
However, in 2008 and up they added a feature for Fine-Grain Password Policies. Essentially more than one password policy may be in effect for a specific user account.
Does anyone have any resources or sample code that takes into account these new FGPP and how I can incorporate them into my existing script?
Thanks
The easiest way is to look at the msDS-ResultantPSO constructed attribute on the specific user in question and get the DN of the password settings object that applies to the user. From there, you can look at the expiry setting on the PSO and combine it with the pwdLastSet value on the user.
If the msDS-ResultantPSO attribute is null on the user, then you should fall back to the domain password policy.
I know this question is almost 4 years old but I wanted to add the code that I got working to solve a similar problem. Additionally, if you aren't able to read from the PSO you need to make sure the user running your code has Read permissions on the PSO in question (this is what caused me the most trouble).
var ad = new PrincipalContext(ContextType.Domain, _domain, _ldapPathOu);
UserPrincipal user = UserPrincipal.FindByIdentity(ad, username);
DirectoryEntry entry = user.GetUnderlyingObject() as DirectoryEntry;
DirectorySearcher mySearcher = new DirectorySearcher(entry);
SearchResultCollection results;
mySearcher.PropertiesToLoad.Add("msDS-ResultantPSO");
results = mySearcher.FindAll();
if (results.Count >= 1)
{
string pso = results[0].Properties["msDS-ResultantPSO"][0].ToString();
//do something with the pso..
DirectoryEntry d = new DirectoryEntry(#"LDAP://corp.example.com/"+ pso);
var searchForPassPolicy = new DirectorySearcher(d);
searchForPassPolicy.Filter = #"(objectClass=msDS-PasswordSettings)";
searchForPassPolicy.SearchScope = System.DirectoryServices.SearchScope.Subtree;
searchForPassPolicy.PropertiesToLoad.AddRange(new string[] {"msDS-MaximumPasswordAge"});
var x = searchForPassPolicy.FindAll();
var maxAge = (Int64)x[0].Properties["msDS-MaximumPasswordAge"][0];
var maxPwdAgeInDays = ConvertTimeToDays(maxAge);
}
Since I don't have enough reputation to comment on #foldinglettuce's answer, I'll provide it in my own answer.
First, you don't need to escape forward slashes as you do for DirectoryEntry with an "#". It's not required and should be removed.
Second, you ask for the msDS-ResultantPSO property to be loaded, excellent. Then you check to make sure a user was found with if (results.Count >= 1), again excellent. Now's where you fail...what if that property is null (and therefore not included)? you can't .ToString(). You need to follow up with a check for that. It should look like something like this...
if (results.Count >= 1)
{
if(results[0].Properties.Contains("msDS-ResultantPSO"))
{
// do stuff
}
}

PrincipalContext.ValidateCredentials always returns FALSE

I have an MVC application that needs to login and verify a user against Active Directory. I am using the PrincipalContext.ValidateCredentials method but always get a authentication of false.
Connecting to the Server is fine. The problem seems to occur in the ValidateCredentials.
Here is my code:
public static bool IsAuthenticated(string domain, string username, string pwd) {
bool IsAuthenticated = false;
try {
PrincipalContext insPrincipalContext =
new PrincipalContext(ContextType.Domain, domain, "DC=c1w,DC=com");
username = "c1w\\" + username;
IsAuthenticated = insPrincipalContext.ValidateCredentials(username, pwd);
}
catch (Exception ex)
{
// Rethrow this exception
ExceptionPolicy.HandleException(ex, "Exception Policy");
}
return IsAuthenticated;
}
Anyone know why this would be happening?
Here's how ValidateCredentials(string, string) works: First, it tries to authenticate with the Negotiate, Signing, and Sealing context options. If this fails, it tries again with SimpleBind and SecureSocketLayer.
The problem is that the NT4 (AKA "legacy", AKA "down-level name") format (DOMAIN\UserName, or more correctly, NetBiosName\SamAccountName) doesn't work with Negotiate. But it does work with SimpleBind.
So what's probably happening when calling the 2-parameter ValidateCredentials() method, is that it first fails using Negotiate because it doesn't like the NT4 format, and then fails again when using simple bind.
During my own testing, I've found that the reason why it fails even after falling back to using simple bind is that it's not only using SimpleBind. It's using SimpleBind plus SecureSocketLayer. This means that it will still fail if the Active Directory server isn't set up correctly to use SSL (a common scenario for test environments).
As was mentioned in one of the comments, you NEVER, NEVER want to use SimpleBind by itself (without SecureSocketLayer), otherwise your passwords are sent over the network in plain text.
In the wild, I've seen that some Active Directory systems don't allow the use of simple binds at all, so you must make it work with Negotiate.
I've found 2 ways to deal with this problem:
1) If everything is happening on the same domain, you should be able to call ValidateCredentials with only the username (SAM account name), leaving out the "DOMAIN\" part. Then, it will work properly the first time with Negotiate.
2) If the domain part is important because there may be multiple domains involved (i.e. Domain1\UserA and Domain2\UserA are different people), then it gets a bit more complicated. In this case what I ended up doing was translating the NT4 name (DOMAIN\User) to "user principal name" format (e.g. LogonName#domain.com). There are a couple different ways to do this. The easiest is probably to use the 3-parameter overload of UserPrincipal.FindByIdentity(), and then grab the value of the UserPrincipalName property on the result. Another way would be to use a DirectorySearcher and query LDAP://domain for the userPrincipalName property of the user with the matching sAMAccountName value. Note: this solution will only work if all the domains involved are in the same forest.
I don't see where you initializes the "pwd" variable
Maybe you should use ContextOption in this method to specify exactly the reqired behaviour. Sorry for too broad response but there is no much details in your question
It seems you are validating the user with domain\userName format. You might want to parse the domain name from userName and user the ValidateCredential.

Categories

Resources