LDAP reset password from outside the domain network C# Error: RPC server is unavailable. (exception from hresult: 0x800706ba)

3

We're trying to Reset LDAP password, its working on development environment but not working on production environment.

Our development environment is inside the Domain and production environment is outside the Domain.

In development to connect LDAP, we have used Domain name like abc.com and production environment we use IPaddress:389, which is already working for LDAP User authentication in both environment. But not working for LDAP reset password.

Error: RPC server is unavailable. (exception from hresult: 0x800706ba)

Developement: (working)

PrincipalContext principalContext =
    new PrincipalContext(ContextType.Domain, "<domain.com>", container: "<DC=domain,DC=com>",
    "<username>", "<password>");
UserPrincipal user = UserPrincipal.FindByIdentity(principalContext, "<LdapUserName>");
// "<username>", "<password>" are Administrative credential.
bool isValid = user.ValidateCredentials("<username>", "<password>");
_logger.Log($"Is Connection: {isValid}");
**// Output: Is Connection: True**
user.UserCannotChangePassword = false;
user.SetPassword("<NewPassword>");
// Password has been successfully reset.

Production: (working) Also we are authenticate LDAP users using below method its working on Production:

Check user has LDAP account or not:

// "<username>", "<password>" are Administrative credential.
var entry = new DirectoryEntry($"LDAP://{"<IP:389>"}", "<username>", "<password>",
    AuthenticationTypes.Secure | AuthenticationTypes.Sealing | AuthenticationTypes.ServerBind);
var search = new DirectorySearcher(entry);
var strFilter = $"(mail={"<UserEmailId>"})";
search.Filter = strFilter;
var result = await Task.Run(() => search.FindOne());
if (result != null)
{
    //IsLdapUser = true;
    //result.Properties["samaccountname"][0]);
}
else
{
    //IsLdapUser = false;
}
// Successfully


// Authenticate LDAP user:
var ldapConnection = new LdapConnection(new LdapDirectoryIdentifier("<IP:389>", false, false));                    
var nc = new NetworkCredential("<LdapUserName>", "<LdapUserPassword>", "<IP:389>");
ldapConnection.Credential = nc;
ldapConnection.AuthType = AuthType.Negotiate;
ldapConnection.Bind(nc);
// Successfully

Production: (not working)

// "<username>", "<password>" are Administrative credential.
PrincipalContext principalContext =
    new PrincipalContext(ContextType.Domain, "<IP:389>", container: "<DC=domain,DC=com>",
    "<username>", "<password>");
UserPrincipal user = UserPrincipal.FindByIdentity(principalContext, "<LdapUserName>");
bool isValid = user.ValidateCredentials("<username>", "<password>");
_logger.Log($"Is Connection: {isValid}");
**// Output: Is Connection: True**
user.UserCannotChangePassword = false;
user.SetPassword("<NewPassword>");
// Error: RPC server is unavailable. (exception from hresult: 0x800706ba)

Also tried with below code (not working)

// "<username>", "<password>" are Administrative credential.
DirectoryEntry de = new DirectoryEntry("<IP:389>","<username>", "<password>", 
AuthenticationTypes.Secure | AuthenticationTypes.Sealing | AuthenticationTypes.ServerBind);
// LDAP Search Filter
DirectorySearcher ds = new DirectorySearcher(de);
ds.Filter = "(&(objectClass=user)(|(sAMAccountName=" + "<LdapUserName>"+ ")))";

// LDAP Properties to Load
ds.PropertiesToLoad.Add("displayName");
ds.PropertiesToLoad.Add("sAMAccountName");
ds.PropertiesToLoad.Add("DistinguishedName");
ds.PropertiesToLoad.Add("CN");

// Execute Search
SearchResult result = await Task.Run(() => ds.FindOne());

string dn = result.Properties["DistinguishedName"][0].ToString();

DirectoryEntry uEntry = result.GetDirectoryEntry();

uEntry.Invoke("SetPassword", new object[] { "<NewPassword>"});  //Set New Password                        
uEntry.CommitChanges();
uEntry.Close();
// Error: RPC server is unavailable. (exception from hresult: 0x800706ba)
c#
asp.net
asp.net-core
authentication
ldap
asked on Stack Overflow May 29, 2020 by Sachin Panchal • edited May 29, 2020 by Divyang Desai

1 Answer

2

The attribute used to modify the password is unicodePwd. That documentation reveals some conditions that must be met for the password to be changed. Primarily, the connection must be encrypted.

Calling .Invoke("SetPassword", ...) actually calls the native Windows IADsUser::SetPassword method. That documentation shows that it automatically attempts a few different ways to encrypt. The exception happens because none of these methods worked.

You can actually modify the unicodePwd attribute directly, without calling SetPassword, which I'll get to, but regardless, you have to resolve the issue of encryption first.

When you're running this from a computer inside the network, AuthenticationTypes.Sealing is enough. As the documentation says, the effect is that it uses Kerberos to encrypt the connection.

But when you're connecting from outside the domain, Kerberos won't work (maybe it will with effort - I'm no Kerberos expert). So the only usable encryption method is SSL. The SetPassword method does actually attempt to use SSL, but clearly it didn't work.

One problem I see right away is that you're using an IP address to connect to the DC, and SSL won't work using an IP address, since the domain name name on the SSL certificate must match the name you are using to access to the server, and the SSL cert will not have the IP address on it. So you will have to change that to use the domain name. If DNS will not resolve the name, you can add it to your hosts file.

Changing that may fix everything. If not, there can be two other issues:

  1. Port 636 is not accessible (a firewall in the way).
  2. The SSL certificate is not trusted on the computer you run this on

LDAP over SSL (LDAPS) works on port 636. You can test this connection in PowerShell:

Test-NetConnection example.com -Port 636

If that fails, fix that first.

Next, check the certificate. You can download the certificate with this PowerShell script:

$webRequest = [Net.WebRequest]::Create("https://example.com:636")
try { $webRequest.GetResponse() } catch {}
$cert = $webRequest.ServicePoint.Certificate
$bytes = $cert.Export([Security.Cryptography.X509Certificates.X509ContentType]::Cert)
set-content -value $bytes -encoding byte -path "certificate.cer"

Change the example.com in the first line to your domain name (leave the https:// and :636). Then you will have a file called certificate.cer in the current directory that you can open and inspect. It will warn you if it is not trusted. If it's not trusted, then you will have to install the root certificate on the server as a Trusted Root Certificate.

If it is already trusted, make sure the "Issued to:" domain name on the cert matches the name you used to connect. In our environment, the SSL certificates are in the name of each domain controller (dc1.example.com), not the domain name (example.com). So I have to target a specific domain controller for LDAPS to work.

Once you get all of that sorted out, your code should work.

If you want to change the unicodePwd attribute directly instead of using SetPassword (which may or may not perform a little faster), you will need to make the original connection via SSL. For example:

DirectoryEntry de = new DirectoryEntry("LDAP://dc1.example.com:636","<username>", "<password>", 
    AuthenticationTypes.Secure | AuthenticationTypes.SecureSocketsLayer | AuthenticationTypes.ServerBind);

Only use AuthenticationTypes.ServerBind if you are targeting a specific DC.

Then you can update the unicodePwd attribute in the very specific way that it wants:

uEntry.Properties["unicodePwd"].Value = Encoding.Unicode.GetBytes("\"NewPassword\"");
uEntry.CommitChanges();

Note that the new password must be enclosed in quotes.

answered on Stack Overflow May 29, 2020 by Gabriel Luci • edited May 29, 2020 by Gabriel Luci

User contributions licensed under CC BY-SA 3.0