Solved

Programmatically forcing a password change

Posted on 2011-03-10
10
2,869 Views
Last Modified: 2012-05-11
I'm writing a small app to make it easy for a non-technical user to reset passwords in a Windows Server 2008 R2 environment with Active Directory.

Here's my reset password function:

        public static bool ResetPassword (string userDN, string newPassword)
        {
            DirectoryEntry directoryEntry = null;

            try
            {
                directoryEntry = new DirectoryEntry(userDN);
                directoryEntry.Invoke("SetPassword", new object[] {newPassword});
                directoryEntry.Properties["LockOutTime"].Value = 0; // unlock account
                //directoryEntry.Properties["pwdLastSet"].Value = 0; // force change at next logon
                directoryEntry.InvokeSet("pwdLastSet", new object[] { 0 });
                directoryEntry.CommitChanges();
               
            }
            catch (Exception)
            {
                return false;
            }
            finally
            {
                if (directoryEntry != null)
                {
                    directoryEntry.Close();
                    directoryEntry.Dispose();
                }
            }
           
            return true;
        }

The setting of pwdLastSet property is having no effect. The user is able to logon but is not being forced to change their password.

All the articles I've found here and on other sites say that setting pwdLastSet to zero works, but it doesn't for me. As you can see from the code, I've tried a couple of ways to set the property value.

Does anyone have any ideas?

Thanks,
Jeff
0
Comment
Question by:jeffeld
[X]
Welcome to Experts Exchange

Add your voice to the tech community where 5M+ people just like you are talking about what matters.

  • Help others & share knowledge
  • Earn cash & points
  • Learn & ask questions
  • 6
  • 4
10 Comments
 
LVL 7

Expert Comment

by:jdavistx
ID: 35100765
Are you sure you value is being saved?

After you commit your changes, try re-reading the value in Debug, and see what it retrieves?

public static bool ResetPassword (string userDN, string newPassword)
public static bool ResetPassword (string userDN, string newPassword)
{
	var directoryEntry = new DirectoryEntry();
	try
	{
		using(directoryEntry = new DirectoryEntry(userDN))
		{
			directoryEntry.Invoke("SetPassword", new object[] {newPassword});
			directoryEntry.Properties["LockOutTime"].Value = 0; // unlock account
			directoryEntry.Properties["pwdLastSet"].Value = 0; // force change at next logon
			directoryEntry.CommitChanges();
			//directoryEntry.Properties["pwdLastSet"].Value = ?
		}
		return true;
	}
	catch (Exception){ return false;}
	finally{ directoryEntry.Close(); }
}

Open in new window


Also, it's possible it may be dependent on your domain controllers in that the change has been applied but the domain controller that particular user uses to login with is not synced with the controller that you made the changes to.
}
0
 

Author Comment

by:jeffeld
ID: 35100819
Thanks for the comment. I'll try checking the property tomorrow.

There is only one DC in this scenario.

Cheers,
Jeff
0
 

Author Comment

by:jeffeld
ID: 35100824
Thanks for the comment. I'll try checking the property tomorrow.

There is only one DC in this scenario.

Cheers,
Jeff
0
Has Powershell sent you back into the Stone Age?

If managing Active Directory using Windows Powershell® is making you feel like you stepped back in time, you are not alone.  For nearly 20 years, AD admins around the world have used one tool for day-to-day AD management: Hyena. Discover why.

 
LVL 7

Expert Comment

by:jdavistx
ID: 35100880
I know your app is simple and with focused intent, but you generally don't want to swallow exceptions.  It might be swallowing the very exception that explains why it's not working!

I'm sure you're aware, but be sure the account that is running the app has the necessary rights to modify AD.  I think you can impersonate network credentials in the app if you need to.
0
 

Author Comment

by:jeffeld
ID: 35101096
Good points. I'll clarify...

1) No exception is getting thrown. The apps UI displays a message if the function returns false (and no message is displayed). I also have Visual Studio set to stop when an exception is thrown (and not just caught). The password itself is reset ok, its just the forcing the user to change it when next logging on that "fails".

2) Yes, the account I'm developing this under (and the two accounts that will ultimately use it) are Domain Admins.

Cheers,
Jeff
0
 
LVL 7

Expert Comment

by:jdavistx
ID: 35103124
I'm curious to see what value you have when retrieving after the commit.
0
 

Accepted Solution

by:
jeffeld earned 0 total points
ID: 35107294
Right. Sorted!

I was at a total dead end with this. No amount of messing about would let me see any details of what pwdLastSet was set to before the reset or after. It seemed to be a totally opaque _ComObject as far as .NET was concerned.

I eventually stumbled across this: http://www.rlmueller.net/UserAttributes.htm and from there a spread sheet with all AD attributes listed. In the comments for pwdLastSet was this little gem:

Last time the password was modified. If this value is set to zero the user must set the password at the next logon if password never expires is not set to True

Low and behold, all the target user accounts for this app have Password Never Expires set. Switch it off and setting pwdLastSet to 0 works!
0
 

Author Comment

by:jeffeld
ID: 35107551
Just for completeness. here is the final bit of code:

    class ADUser
    {
        private const string UserAccountControl = "userAccountControl";
        private const string SetPassword = "SetPassword";
        private const string LockOutTime = "LockOutTime";
        private const string PwdLastSet = "pwdLastSet";
        private const int DontExpirePassword = 0x10000;

        public static bool ResetPassword (string userDN, string newPassword, out bool removedPasswordNeverExpires)
        {
            DirectoryEntry directoryEntry = null;
            removedPasswordNeverExpires = false;

            try
            {
                directoryEntry = new DirectoryEntry(userDN);

                // If you ever need to know anything about AD user attributes,
                // go here: http://www.rlmueller.net/UserAttributes.htm
                
                // We need to check if the account's password is set to Never Expire.
                // If it is, we need to remove it as you cannot force the user to change their 
                // password at next logon AND have the password to never expire.

                int userAccountControl = (int)directoryEntry.Properties[UserAccountControl].Value;

                bool passwordNeverExpires = (userAccountControl & DontExpirePassword) == DontExpirePassword;
                if (passwordNeverExpires)
                {
                    // Mask off the password never expires bit and save the changes
                    // back to the directory.

                    userAccountControl = userAccountControl & ~DontExpirePassword;
                    directoryEntry.Properties[UserAccountControl].Value = userAccountControl;
                    directoryEntry.CommitChanges();

                    removedPasswordNeverExpires = true;
                }

                // Set the password, unlock the account and force the user to change password at next logon

                directoryEntry.Invoke(SetPassword, new object[] {newPassword});
                directoryEntry.Properties[LockOutTime].Value = 0; // unlock account
                directoryEntry.Properties[PwdLastSet].Value = 0; // force change at next logon
                
                directoryEntry.CommitChanges();

                // NB: We do not attempt to restore the password never expires bit to the
                // userAccountControl flags. To do so would remove the change password at next logon
                // account option, as the two are mutually exclusive.
                
            }
            catch (Exception)
            {
                return false;
            }
            finally
            {
                if (directoryEntry != null)
                {
                    directoryEntry.Close();
                    directoryEntry.Dispose();
                }
            }
            
            return true;
        }
    }
}

Open in new window

0
 
LVL 7

Expert Comment

by:jdavistx
ID: 35108411
Good deal!

For brevity, you may consider something like the following

class ADUser
{
	private enum ADAttribute
	{
		userAccountControl,
		SetPassword,
		LockOutTime,
		pwdLastSet
	}

	private const int dontExpirePassword = 0x10000;

	public static bool ResetPassword (string userDN, string newPassword, out bool removedPasswordNeverExpires)
	{
		var directoryEntry = new DirectoryEntry();
		removedPasswordNeverExpires = false;

		try
		{
			using(directoryEntry = new DirectoryEntry(userDN))
			{
				int userAccountControl = (int)directoryEntry.Properties[ADAttribute.userAccountControl.ToString()].Value;

				if ((userAccountControl & dontExpirePassword) == dontExpirePassword)
				{
					userAccountControl = userAccountControl & ~dontExpirePassword;
					directoryEntry.Properties[ADAttribute.userAccountControl.ToString()].Value = userAccountControl;
					directoryEntry.CommitChanges();
					removedPasswordNeverExpires = true;
				}

				directoryEntry.Invoke(ADAttribute.SetPassword.ToString(), new object[] {newPassword});
				directoryEntry.Properties[ADAttribute.LockOutTime.ToString()].Value = 0;
				directoryEntry.Properties[ADAttribute.pwdLastSet.ToString()].Value = 0;
				directoryEntry.CommitChanges();
			}
		}
		catch (Exception){ return false; }
		finally{ directoryEntry.Close(); }
				
		return true;
	}
}

Open in new window


If you use an object that implements IDisposable (such as DirectoryEntry), you can enclose it within a using(){} block, and it will close/dispose itself upon reaching the end of the block.

In the finally block, you can simply do .Close() to ensure that any resources associated with the DirectoryEntry are released.  The Close() method will also call Dispose()
0
 

Author Closing Comment

by:jeffeld
ID: 35145252
None of the websites I initially came across mentioned the preconditions required for using the pwdLastSet AD attribute.

To reiterate: setting pwdLastSet to 0 only works if the account password has an expiry date. If the password is set to never expire, setting pwdLastSet to force a password change at next logon has no effect.
0

Featured Post

Office 365 Training for Admins - 7 Day Trial

Learn how to provision tenants, synchronize on-premise Active Directory, implement Single Sign-On, customize Office deployment, and protect your organization with eDiscovery and DLP policies.  Only from Platform Scholar.

Question has a verified solution.

If you are experiencing a similar issue, please ask a related question

Suggested Solutions

Title # Comments Views Activity
Disable the weekends on datepicker control 6 52
Domain controller sizing 7 62
Intermittent OleDbConnection Error 20 53
I'm being stupid with my powershell 2 28
This article demonstrates probably the easiest way to configure domain-wide tier isolation within Active Directory. If you do not know tier isolation read https://technet.microsoft.com/en-us/windows-server-docs/security/securing-privileged-access/s…
Recently, Microsoft released a best-practice guide for securing Active Directory. It's a whopping 300+ pages long. Those of us tasked with securing our company’s databases and systems would, ideally, have time to devote to learning the ins and outs…
This tutorial will walk an individual through the process of transferring the five major, necessary Active Directory Roles, commonly referred to as the FSMO roles from a Windows Server 2008 domain controller to a Windows Server 2012 domain controlle…
Attackers love to prey on accounts that have privileges. Reducing privileged accounts and protecting privileged accounts therefore is paramount. Users, groups, and service accounts need to be protected to help protect the entire Active Directory …

759 members asked questions and received personalized solutions in the past 7 days.

Join the community of 500,000 technology professionals and ask your questions.

Join & Ask a Question