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.
Related
I am using PrincipalContext.ValidateCredentials method from System.DirectoryServices.AccountManagement namespace to validate user credentials against Active
Directory LDAP server. Sample of code:
private bool CheckIfCredentialsAreValidInDomain(string pLogin, string pPassword)
{
bool areCredentialsValidInDomain = true;
using (PrincipalContext context = new PrincipalContext(ContextType.Domain))
{
areCredentialsValidInDomain = context.ValidateCredentials(login, password);
}
return areCredentialsValidInDomain;
}
There is one domain and several (6 or more) DC in customer's environment. I don't pass DC name into PrincipalContext constructor - assuming DC Locator Service is doing its job - it is not important for me which particular DC is used from list of available DCs . Everything works great but I have have case of user who doesn't directly log on the domain (before starting application where this validanting is used) but his computer is physically connected to the customer's network.
This user's domain account has been recently disabled. Reason: he didn't log in to the domain for the last X months. But until then he was using app on daily basis so ValidateCredentials method was being called and returning true. But for unclear reason this action was "transaparent" for DC and this validation was not marked.
So how does ValidateCredentials work? Does it set LastLogon and lastLogonTimestamp user's attribute or just tells us if credentials are valid or not? Does it register any Event log entry on DC?
The source code for PrincipalContext is available now. ValidateCredentials() calls CredentialValidator.Validate() (an internal class).
That eventually calls lockedLdapBind(), which calls LdapConnection.Bind() with the credentials.
It does actually test the credentials against a server. So either that part of your code is not actually being run, or the account being tested isn't really disabled.
Something I noticed in your code is that you're passing the variables login and password to ValidateCredentials. However, the parameters for your method are called pLogin and pPassword. Is that just a typo in your question, or is that really how it is in your code? If that is accurate, then you're not actually testing the credentials passed to your method.
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 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);
}
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.
Given the following code, why do I get a PrincipalExistsException when I know for a fact that the principal doesn't exist and isn't actually made?
public UserPrincipal Add(
string givenName,
string surname,
string domain) {
UserPrincipal principal = new UserPrincipal(context: base.Context) {
Enabled = true
};
if (!String.IsNullOrEmpty(givenName) && !String.IsNullOrEmpty(surname) && !String.IsNullOrEmpty(domain)) {
this.RenameInternal(principal: principal, givenName: givenName, surname: surname, domain: domain);
principal.Save();
};
return principal;
}
I can confirm that the RenameInternal() method works just fine because it get's called by a method named Rename(). So, there must be an issue with how the object is created and/or saved, but I don't know how to find out where the error is. This seems like simple enough code...
Looking through the domain controller (Windows Server 2008 R2) I can't find the "newly" create principal anywhere, so I'm assuming that it's not being created and that the exception is lying to me somehow.
I'd appreciate any help on this. Thanks in advance.
Ok, I figured it out. The SamAccountName was what was causing the exception to be thrown (thank you Microsoft for the ever helpful error messages). Anyway, it was the issue because it was trying to set it to a name that already exists. I modified it by tossing in a middle initial and it's working fine, or at-least until I get people with the same first initial, middle initial and last name.
The only thing I can think of off hand would be the following
Whatever ID your application is running under, needs to have "write" access to AD. Pretty much any ID can query AD but only ID's explicitly granted the privilege can write to it.