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
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.
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.
I am trying to determine if a user account in AD is enabled. For this I use the following code:
string domain = "my domain";
string group = "my security group";
string ou = "my OU";
//init context
using (var cnt= new PrincipalContext(ContextType.Domain, domain))
{
//find the necessary security group
using (GroupPrincipal mainGroup
= GroupPrincipal.FindByIdentity(cnt, IdentityType.Guid, group))
{
if (mainGroup != null)
{
//get the group's members
foreach (var user in mainGroup.GetMembers()
.OfType<UserPrincipal>()
.Where(u => u.DistinguishedName.Contains(ou)))
{
//ensure that all the info about the account is loaded
//by using FindByIdentity as opposed to GetMembers
var tmpUser= UserPrincipal.FindByIdentity(cnt,
user.SamAccountName);
//actually I could use `user` variable,
//as it gave the same result as `tmpUser`.
//print the account info
Console.WriteLine(tmpUser.Name + "\t" +
tmpUser.Enabled.HasValue + "\t" +
tmpUser.Enabled.Value);
}
}
}
}
The problem is, when I run this code under an administrative account, I get the real result, while when I run it under a non-priviledged account, user.Enabled returns false for some of the accounts, while it should be true.
The only similar q&a I managed to find are
UserPrincipal.Enabled returns False for accounts that are in fact enabled?
Everything in Active Directory via C#.NET 3.5 (Using System.DirectoryServices.AccountManagement)
which do not help here.
Why is that so? What are my options to get this info under a non-priviledged account?
Here is another approach: How to determine if user account is enabled or disabled:
private bool IsActive(DirectoryEntry de)
{
if (de.NativeGuid == null)
return false;
int flags = (int)de.Properties["userAccountControl"].Value;
if (!Convert.ToBoolean(flags & 0x0002))
return true;
else
return false;
}
Same approach is described in Active Directory Objects and C#.
However when running under an unpriviledged user account, userAccountControl attribute is null and it's not possible to determine the state of the account.
The workaround here is to use PrincipalContext Constructor, specifying the credentials of a user with enough priviledges to access AD.
It stays unclear to me, why the unpriviledged user had access to AD at all, and couldn't get values of some certain account attributes. Probably this has nothing to do with C#, and should be configured in AD...
You'll need to delegate permissions in Active Directory for the accounts that will be performing the AD queries. This is what I had to do for my applications to work (though we are performing other administrative tasks on user accounts).
Check Here for instructions on how to delegate permissions(or see blockquote below).
You may referred the following procedure to run the delegation:
Start the delegation of control wizard by performing the following steps:
Open Active Directory Users and Computers.
In the console tree, double click the domain node.
In the details menu, right click the organizational unit, click delegate control, and click next.
Select the users or group to which you want to delegate common administrative tasks. To do so, perform the following steps:
On the Users or Groups page, click Add.
In the select Users, computers or Groups, write the names of the users and groups to which you have to delegate control of the organizational unit, click OK. And click next.
Assign common tasks to delegate. To do so perform the following common tasks.
On the tasks to delgate page, click delegate the following common tasks.
On the tasks to delegate page, select the tasks you want to delegate, and click OK. Click Finish
For Example: To delegate administrator to move user/computer objects, you can use advance mode in AD User and Computer and run delegation. It should have write privilege in both OU for the object moving. For writing new values, the administrators account should have delegated values on the user account (Full privilege in specific OU as well.
Something else worth looking into is if the accounts have the userAccountControl attribute. I've heard that accounts missing this attribute may not report correctly. In most scenarios this attribute should be set to NormalAccount.
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
}
}
I have a client that's utilizing a windows service I wrote that polls a specified active directory LDAP server for users in specified groups within that LDAP server.
Once it finds a user, it fills out the user information (i.e. username, email, etc.) and attempts to retrieve the user's domain within that LDAP server.
When I attempt to retrieve the user's domain for this specific client, I'm hitting a DirectoryServicesCOMException: Logon failure: unkonwn user name or bad password.
This exception is being thrown when I attempt to reference a property on the RootDSE DirectoryEntry object I instantiate.
This client has a Forest with two roots, setup as follows.
Active Directory Domains and Trusts
ktregression.com
ktregression.root
I assume this is the issue.
Is there any way around this? Any way to still retrieve the netbiosname of a specific domain object without running into this exception?
Here is some sample code pointing to a test AD server setup as previously documented:
string domainNameLdap = "dc=tempe,dc=ktregression,dc=com";
DirectoryEntry RootDSE = new DirectoryEntry (#"LDAP://10.32.16.6/RootDSE");
DirectoryEntry servers2 = new DirectoryEntry (#"LDAP://cn=Partitions," + RootDSE.Properties["configurationNamingContext"].Value ); //*****THIS IS WHERE THE EXCEPTION IS THROWN********
//Iterate through the cross references collection in the Partitions container
DirectorySearcher clsDS = new DirectorySearcher(servers2);
clsDS.Filter = "(&(objectCategory=crossRef)(ncName=" + domainNameLdap + "))";
clsDS.SearchScope = SearchScope.Subtree;
clsDS.PropertiesToLoad.Add("nETBIOSName");
List<string> bnames = new List<string>();
foreach (SearchResult result in clsDS.FindAll() )
bnames.Add(result.Properties["nETBIOSName"][0].ToString());
It seems that the user account with which the Active Directory tries to authenticate "you" does not exist as your DirectoryServicesCOMException reports it.
DirectoryServicesCOMException: Logon failure: unkonwn user name or bad password.
Look at your code sample, it seems you're not using impersonation, hence the security protocol of the Active Directory take into account the currently authenticated user. Make this user yourself, then if you happen not to be defined on both of your domain roots, one of them doesn't know you, which throws this kind of exception.
On the other hand, using impersonation might solve the problem here, since you're saying that your Windows Service account has the rights to query both your roots under the same forest, then you have to make sure the authenticated user is your Windows Service.
In clear, this means that without impersonation, you cannot guarantee that the authenticated user IS your Windows Service. To make sure about it, impersonation is a must-use.
Now, regarding the two roots
ktregression.com;
ktregression.root.
These are two different and independant roots. Because of this, I guess you should go with two instances of the DirectoryEntry class fitting one for each root.
After having instantiated the roots, you need to search for the user you want to find, which shall be another different userName than the one that is impersonated.
We now have to state whether a user can be defined on both roots. If it is so, you will need to know when it is better to choose one over the other. And that is of another concern.
Note
For the sake of simplicity, I will take it that both roots' name are complete/full as you mentioned them.
private string _dotComRootPath = "LDAP://ktregression.com";
private string _dotRootRootPath = "LDAP://ktregression.root";
private string _serviceAccountLogin = "MyWindowsServiceAccountLogin";
private string _serviceAccountPwd = "MyWindowsServiceAccountPassword";
public string GetUserDomain(string rootPath, string login) {
string userDomain = null;
using (DirectoryEntry root = new DirectoryEntry(rootPath, _serviceAccountLogin, _serviceAccountPwd))
using (DirectorySearcher searcher = new DirectorySearcher()) {
searcher.SearchRoot = root;
searcher.SearchScope = SearchScope.Subtree;
searcher.PropertiesToLoad.Add("nETBIOSName");
searcher.Filter = string.Format("(&(objectClass=user)(sAMAccountName={0}))", login);
SearchResult result = null;
try {
result = searcher.FindOne();
if (result != null)
userDomain = (string)result.GetDirectoryEntry()
.Properties("nETBIOSName").Value;
} finally {
dotComRoot.Dispose();
dotRootRoot.Dispose();
if (result != null) result.Dispose();
}
}
return userDomain;
}
And using it:
string userDomain = (GetUserDomain(_dotComRoot, "searchedLogin")
?? GetUserDomain(_dotRootRoot, "searchedLogin"))
?? "Unknown user";
Your exception is thrown only on the second DirectoryEntry initilization which suggests that your default current user doesn't have an account defined on this root.
EDIT #1
Please see my answer to your other NetBIOS Name related question below:
C# Active Directory: Get domain name of user?
where I provide a new and probably easier solution to your concern.
Let me know if you have any further question. =)
I believe that the DirectoryEntry has properties to specify for an AD account that can perform LDAP queries or updates, you can also delegate that control down from your parent domain.