I am trying to implement a ChangePassword functionality on ActiveDirectory through the UserPrincipal. The code looks like this:
using System.DirectoryServices.AccountManagement;
private PrincipalContext Context { get; set; }
...
Context = new PrincipalContext(ContextType.Domain,
AdDomain,
AdRoot,
ContextOptions.SimpleBind,
AdUsername,
AdPassword)
...
public bool ChangePassword(string login, string password, string newPassword, out string message)
{
using (var foundUser = UserPrincipal.FindByIdentity(Context, IdentityType.SamAccountName, login))
{
try
{
foundUser.ChangePassword(password, newPassword);
foundUser.Save();
}
catch (Exception e)
{
message = e.Message;
return false;
}
return true;
}
}
When I try to test this on my Windows 10 machine I get an exception on ChangePassword, System.Runtime.InteropServices.COMException - One or more input parameters are invalid.
However, when I run this exact same code in the same project while connecting to the same AD domain on my Windows 7 machine it runs without error and changes the password. What could be causing this different behavior on the environments, and why does that error happen?
When the Context is created, make sure to set the ContextOptions to ContextOptions.Negotiate .
If you have mentioned ContextOptions.SimpleBind SetPassword may not work.
PrincipalContext oPrincipalContext = new PrincipalContext
(ContextType.Domain, "Name", "DefaultOU(if required)", ContextOptions.Negotiate,
"Service Account(if required)", "Service password");
Related
I have a Windows Service that provides data through WCF to a Windows Forms application. The service takes care also of user authentication, validating user password using LDAP over the company Active Directory servers.
The problem is that it works for weeks (even months), than something happens and the LDAP user authentication fails with the following exception until I restart the service:
System.DirectoryServices.AccountManagement.PrincipalServerDownException: The server could not be contacted.
---> System.DirectoryServices.Protocols.LdapException: The LDAP server is unavailable.
at System.DirectoryServices.Protocols.LdapConnection.Connect()
at System.DirectoryServices.Protocols.LdapConnection.SendRequestHelper(DirectoryRequest request, Int32& messageID)
at System.DirectoryServices.Protocols.LdapConnection.SendRequest(DirectoryRequest request, TimeSpan requestTimeout)
at System.DirectoryServices.Protocols.LdapConnection.SendRequest(DirectoryRequest request)
at System.DirectoryServices.AccountManagement.PrincipalContext.ReadServerConfig(String serverName, ServerProperties& properties)
--- End of inner exception stack trace ---
at System.DirectoryServices.AccountManagement.PrincipalContext.ReadServerConfig(String serverName, ServerProperties& properties)
at System.DirectoryServices.AccountManagement.PrincipalContext.DoServerVerifyAndPropRetrieval()
at System.DirectoryServices.AccountManagement.PrincipalContext..ctor(ContextType contextType, String name, String container, ContextOptions options, String userName, String password)
at System.DirectoryServices.AccountManagement.PrincipalContext..ctor(ContextType contextType, String name, String userName, String password)
at SMSTModel.Authentication.ActiveDirectory.IsUserAllowed(String username, String password)
The service restart fixes the problem.
public static bool IsUserAllowed(string username, string password)
{
String localDomain = Domain.GetComputerDomain().Name;
string userDomain = null;
string user = username;
if (user.Contains(#"\"))
{
userDomain = user.Substring(0, user.IndexOf("\\"));
user = user.Substring(user.IndexOf("\\") + 1);
}
userDomain = userDomain != null ? userDomain : localDomain;
using (PrincipalContext pc = new PrincipalContext(ContextType.Domain, userDomain, user, password))
{
bool credOk = pc.ValidateCredentials(user, password);
if (!credOk)
return false;
using (UserPrincipal userP = UserPrincipal.FindByIdentity(pc, user))
{
if (userP != null)
{
using (PrincipalContext pc1 = new PrincipalContext(ContextType.Domain, localDomain))
{
using (GroupPrincipal groupPrincipal = new GroupPrincipal(pc1))
{
groupPrincipal.Name = "APP_*";
using (PrincipalSearcher principalSearcher = new PrincipalSearcher(groupPrincipal))
foreach (Principal found in principalSearcher.FindAll())
{
if (found.Name == "APP_Group" && found is GroupPrincipal && userP.IsMemberOf((GroupPrincipal)found))
{
return true;
}
}
}
}
}
}
}
return false;
}
Any idea on why it happens and how to fix it?
It looks like the exception is happening here:
using (PrincipalContext pc = new PrincipalContext(ContextType.Domain, userDomain, user, password))
So do you know what the userDomain being used is? It seems like you might have more than one domain in your environment, so does it happen with all of your domains, or only one?
In our environment, I've seen cases where our AD admins decommission a domain controller, but for some reason that server still shows up in DNS. In other words, if I do a DNS lookup in the command line:
nslookup example.com
one of the IPs is for a decommissioned DC.
That's a possibility in your case. If it picked a bad IP address, then restarting the application would make it do another DNS lookup, which might return a different IP address at the top of the list and things would work again.
To get to the bottom of this, you will really have to observe what is going on at the time it stops working. If you haven't already, install Wireshark on your server. When it stops working, use Wireshark to look for traffic using port 389 (the default LDAP port) and see which IP it's trying to connect to.
I am using c# code in MVC web app to validate a user and find the list of user groups that particular user belongs to and using the below code
try
{
List<string> user_groups= new List<string>(); //save the groups the user belongs to
using (PrincipalContext ctx = new PrincipalContext(ContextType.Domain, "mydomain.com",model.Email,model.Password))
{
bool valid=ctx.ValidateCredentials(model.Email, model.Password); //validate the user
if(valid)
{
UserPrincipal user = UserPrincipal.FindByIdentity(ctx, model.Email);
if (user != null)
{
// get the user's groups
var groups = user.GetAuthorizationGroups();
foreach (GroupPrincipal group in groups)
{
// save those groups to session for further processing after login
if ((bool)group.IsSecurityGroup)
{
user_groups.Add(group.Name);
}
}
}
_groups = string.Join(",", user_groups);
ViewBag.Message = _groups;
}
else
{
ViewBag.Message = "Error while validating";
}
ctx.Dispose();
}
}
catch (PrincipalServerDownException)
{
//If server is down or some exception happends ,
// ad_verification = false;
ViewBag.Message = "Error at groups fetching as server is down ";
}
catch (Exception ex)
{
ViewBag.Message = "Error at groups fetching as "+ex.Message;
}
I deployed this to server and try to login as user1 and all went well The code validate the user credentials and returned the list of user groups the user1 belongs to
Now i logged in as user2 on server , then it returned the below error
Error at groups fetching as Multiple connections to a server or shared resource by the same user, using more than one user name, are not allowed. Disconnect all previous connections to the server or shared resource and try again.
It looks like i can still login as user1, but for all other users the error is same as above . While testing on local IIS no such problems
Any known reasons why the above lines is breaking for second user onwards and any suggestions to resolve this
You didn't say which line is throwing the exception, but it might not like that you're using the user's credentials to pull all the data.
If you're running this from a computer that is joined to the same domain (or a trusted domain) then you don't need to put the credentials here:
using (PrincipalContext ctx = new PrincipalContext(ContextType.Domain, "mydomain.com"))
The ValidateCredentials() line will validate the credentials, and everything else will be done with the credentials that the application is running under.
On a side note, you don't need to call ctx.Dispose() when ctx is the subject of your using block. The whole purpose of using is that it will call Dispose() after it leaves the using block. Take a look at the documentation.
I'm writing a program (WinForms, C#) that runs on a Win 7 client machine. It obtains credentials from the user (user id, password, and subdomain name) and uses them to authenticate (via Active Directory) to other servers to which the program remotely connects. The other servers are on a domain different than the domain the Win 7 client machine is on.
Using the NetworkCredential, LdapDirectoryIdentifier, and LdapConnection classes, I can test the credentials with no more than the user id, password, and subdomain name (See answer for S.O. Why does Active Directory validate last password?).
For example, for the user account ssmith#xyz.gov, I need only provide ssmith (user id), the password for ssmith, and xyz (the subdomain). I don't need to provide the top-level domain name (gov in this case).
Now, I want to programmatically obtain the top-level domain name for this user account (gov in this case). I've examined the properties and methods of the NetworkCredential, LdapDirectoryIdentifier, and LdapConnection classes. I've looked over the other classes in the System.DirectoryServices.Protocols Namespace. I don't see a way to programmatically obtain the top-level domain name.
Given user id, password, and subdomain name, how can I obtain the top-level domain name for a user account?
Here is my code. Given a user account of ssmith#xyz.gov, My call looks like this (asterisks represent a SecureString password)
bool result = ValidateCredentials("ssmith","******", "xyz");
Here is my code for the method.
private const int ERROR_LOGON_FAILURE = 0x31;
private bool ValidateCredentials(string username, SecureString ssPassword, string domain)
{
//suports secure string
NetworkCredential credentials = new NetworkCredential(username, ssPassword, domain);
LdapDirectoryIdentifier id = new LdapDirectoryIdentifier(domain);
using (LdapConnection connection = new LdapConnection(id, credentials, AuthType.Kerberos))
{
connection.SessionOptions.Sealing = true;
connection.SessionOptions.Signing = true;
try
{
// The only way to test credentials on a LDAP connection seems to be to attempt a
// Bind operation, which will throw an exception if the credentials are bad
connection.Bind();
}
catch (LdapException lEx)
{
credentials = null;
id = null;
if (ERROR_LOGON_FAILURE == lEx.ErrorCode)
{
return false;
}
throw;
}
}
credentials = null;
id = null;
return true;
}
After a successful bind, then the full DNS name of the domain will be in the LdapConnection object:
var domain = connection.SessionOptions.DomainName;
In this case, that would be "xyz.gov". If you need just "gov", then you can just take everything after the last dot:
var tld = domain.Substring(domain.LastIndexOf('.') + 1);
I'm trying to authenticate users in an application using a domain controller with code like this:
PrincipalContext pcon = new PrincipalContext(ContextType.Domain, domain);
password_ok = pcon.ValidateCredentials(username, password);
That works fine as long as the domain server is online but if there is no domain server the previous code fails with a PrincipalServerDownException exception.
What I'm trying to do is to make my application use the windows cached credentials if the server is down just like Windows do: you may login into windows with a domain user even if the domain server is down.
Can I do that in C#?
Thank you very much in advance.
For posterity, this is how I solved my problem:
bool dmainNotAvailable = false;
PrincipalContext pcon = null;
try
{
pcon = new PrincipalContext(ContextType.Domain, domain);
}
catch (System.DirectoryServices.AccountManagement.PrincipalServerDownException ex)
{
domainNotAvailable = true;
try
{
pcon = new PrincipalContext(ContextType.Machine, Environment.MachineName);
}
catch (Exception ex2)
{
throw new Exception(ex2);
}
}
string realUserName = !domainNotAvailable ? username : $"{domain}\\{username}";
passwordOk = pcon.ValidateCredentials(realUserName, password);
This will also work:
string userName = $"{domain}\\{user}";
PrincipalContext pcon = new PrincipalContext(ContextType.Machine, Environment.MachineName);
passworkOk = pcon.ValidateCredentials(userName, password);
The second solution will try lo login using the cached credentials and if there is no info then will try to login using the domain controller.
On both solutions the domain user must be added first to the local computer in order to have the credentials cached.
That's all.
I want to be able to validate that the credentials are correct for a particular user.
public static void DomainVerification(string username, string password)
{
bool valid = false;
using (PrincipalContext context = new PrincipalContext(ContextType.Domain, "domain.com"))
{
valid = context.ValidateCredentials(username, password);
}
}
I'm not quite sure what I am doing wrong, if it's an invalid domain or invalid user/pass combo..
Within powershell, I am validating the domain with wmic computersystem get domain and running whomai to get the proper username.
What could be causing valid to always be false?