.netpasswordsadamlds

AD LDS (ADAM) ChangePassword over SSL


I have been searching the internet for days trying to solve this problem.

I am working on a project where I have a requirement of allowing the user to change their password using an ASP.NET web application.

I must use "ChangePassword" and not "SetPassword" since I must enforce password history, and not allow any users in the LDS any more privileges than they need. I am trying to complete this task on a dev environment. I have two machines "Server1" (LDS, PingFederate, CA), and "Server2" (IIS). I thought I might be running into problems because I did not have SSL setup between the two boxes, so I spent half the day yesterday setting up a CA and creating certificates for both machines. I am fairly certain it is working since I am no longer seeing any errors in the error log, and I can login to the LDS using LDP using port 636 with SSL checked. I should also mention that these machines are not in a domain environment. I have edited the hosts file on all machines on the test network.

I have tried different variations of code:

public static bool ChangePassword(string email, string pwd, string newPwd)
{
    DirectoryEntry user  = GetCN(email);
    string username = user.Properties["cn"][0].ToString();

    DirectoryEntry de = new DirectoryEntry();
    de.AuthenticationType = AuthenticationTypes.Secure | AuthenticationTypes.SecureSocketsLayer;
    de.Path = user.Path;
    de.Username = username;
    de.Password = pwd;
    try
    {
        Object obj = de.NativeObject;
        de.Options.PasswordEncoding = PasswordEncodingMethod.PasswordEncodingSsl;
        de.Options.PasswordPort = Convert.ToInt32(ConfigurationManager.AppSettings["LDAPPort_ExternalUsers"]);
        de.Invoke("ChangePassword", new object[] { pwd, newPwd });
        de.CommitChanges();
        return true;
    }
    catch (Exception ex)
    {
        return false;
    }
}

public static bool ChangePassword(string email, string pwd, string newPwd)
{
    DirectoryEntry user  = GetCN(email);
    string username = user.Properties["cn"][0].ToString();

    DirectoryEntry de = new DirectoryEntry(user.Path, username, pwd, AuthenticationTypes.Secure | AuthenticationTypes.SecureSocketsLayer);
    try
    {
        de.Options.PasswordEncoding = PasswordEncodingMethod.PasswordEncodingSsl;
        de.Options.PasswordPort = Convert.ToInt32(ConfigurationManager.AppSettings["LDAPPort_ExternalUsers"]);
        de.Invoke("ChangePassword", new object[] { pwd, newPwd });
        return true;
    }
    catch (Exception ex)
    {
        return false;
    }
}

public static bool ChangePassword(string email, string pwd, string newPwd)
{
    DirectoryEntry userInc  = GetCN(email);
    string username = userInc.Properties["cn"][0].ToString();

    using (DirectoryEntry searchRoot = new DirectoryEntry(ConfigurationManager.AppSettings["LDAPConnectionString_ExternalUsers"], username, pwd, AuthenticationTypes.SecureSocketsLayer | AuthenticationTypes.Secure))
    using (DirectorySearcher ds = new DirectorySearcher(searchRoot))
    {
        ds.Filter = "(|(objectCategory=user)(cn=" + username + "))";
        SearchResult sr = ds.FindOne();
        if (sr != null)
        {
            using (DirectoryEntry user = sr.GetDirectoryEntry())
            {
                user.Options.PasswordEncoding = PasswordEncodingMethod.PasswordEncodingClear;
                user.Options.PasswordPort = Convert.ToInt32(ConfigurationManager.AppSettings["LDAPPort_ExternalUsers"]);
                //user.Invoke("SetOption", new object[] { 6, Convert.ToInt32(ConfigurationManager.AppSettings["LDAPPort_ExternalUsers"]) });
                //user.Invoke("SetOption", new object[] { 7, 1 });
                user.Invoke("ChangePassword", new object[] { pwd, newPwd });
            }
            return true;
        }
    }
    return false;
}

I get an exception on the first version at Object obj = de.NativeObject;. I was using this to determine if a bind was happening correctly, and was inserted as a debugging step since this is how I was authenticating a user over port 389. The exception is "Logon failure: unknown user name or bad password."

I get an exception on the second version at de.Options.PasswordEncoding = PasswordEncodingMethod.PasswordEncodingSsl; The exception is "Logon failure: unknown user name or bad password."

I get an exception on the third version at SearchResult sr = ds.FindOne(); The exception is "Logon failure: unknown user name or bad password."

If I do attempt to run this code under port 389 with AuthenticatioTypes.None | AuthenticationTypes.FastBind, it will fail at de.Invoke("ChangePassword", new object[] { pwd, newPwd }); with the exception "Unknown Name". I would really like to get this running under SSL or at least not transmit any passwords in the clear. I do have the site running over HTTPS. I did try modifying the dsHeuristics value on the LDS so that I could do password changes over a non-SSL connection, but that did not work either.

Any suggestions that anybody might have would be much appreciated.


Solution

  • I was able to finally get the password change working. I used the information presented on http://www.informit.com/articles/article.aspx?p=474649&seqNum=4

    I used these three functions:

    private static DirectoryConnection GetConnection(string server, NetworkCredential credential, bool useSsl)
    {
    LdapConnection connection =
      new LdapConnection(server);
    
         if (useSsl)
         {
              connection.SessionOptions.SecureSocketLayer = true;
         }
         else
         {
              connection.SessionOptions.Sealing = true;
         }
    
         connection.Bind(credential);
         return connection;
    }
    
    private static void ChangePassword(DirectoryConnection connection, string userDN, string oldPassword, string newPassword)
    {
         DirectoryAttributeModification deleteMod = new DirectoryAttributeModification();
         deleteMod.Name = "unicodePwd";
         deleteMod.Add(GetPasswordData(oldPassword));
         deleteMod.Operation= DirectoryAttributeOperation.Delete;
    
         DirectoryAttributeModification addMod = new DirectoryAttributeModification();
         addMod.Name = "unicodePwd";
         addMod.Add(GetPasswordData(newPassword));
         addMod.Operation = DirectoryAttributeOperation.Add;
    
         ModifyRequest request = new ModifyRequest(userDN, deleteMod, addMod);
    
         DirectoryResponse response = connection.SendRequest(request);
    }
    
    private static byte[] GetPasswordData(string password)
    {
         string formattedPassword;
         formattedPassword = String.Format("\"{0}\"", password);
         return (Encoding.Unicode.GetBytes(formattedPassword));
    }
    

    I called the function like so from the code behind of the page that allows password changes:

    NetworkCredential credential = new NetworkCredential("[user with access to the LDS", "pwd");
                DirectoryConnection connection;
                try
                {
                    //Setup our connection
                    connection = ADAuth.GetConnection(ConfigurationManager.AppSettings["LDAPServer_ExternalUsers"] + ":" + ConfigurationManager.AppSettings["LDAPPort_ExternalUsers"], credential, true);
    
                    //Attempt to change the password
                    ADAuth.ChangePassword(connection, ADAuth.GetDN(userID.Text).Properties["distinguishedName"].Value.ToString(), currPass.Text, newPass.Text);
    
                    //Send success message to user
                    ErrorLit.Text = "<p>Password change successful!</p>";
                    //Dispose the connection
                    IDisposable disposable = connection as IDisposable;
                    if (disposable != null)
                        disposable.Dispose();
                }
                catch (Exception ex)
                {
                    //There was an error, tell the user
                    errors += "<li>Error changing password</li>";
                    ErrorLit.Text = errors + "</ul>";
                    return;
                }