Solved

Programmatically forcing a password change

Posted on 2011-03-10
10
2,700 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
  • 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
 
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
Enabling OSINT in Activity Based Intelligence

Activity based intelligence (ABI) requires access to all available sources of data. Recorded Future allows analysts to observe structured data on the open, deep, and dark web.

 
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

How to improve team productivity

Quip adds documents, spreadsheets, and tasklists to your Slack experience
- Elevate ideas to Quip docs
- Share Quip docs in Slack
- Get notified of changes to your docs
- Available on iOS/Android/Desktop/Web
- Online/Offline

Join & Write a Comment

[b]Ok so now I will show you how to add a user name to the description at login. [/b] First connect to your DC (Domain Controller / Active Directory Server) SET PERMISSIONS FOR SCRIPT TO UPDATE COMPUTER DESCRIPTION TO USERNAME 1. Open Active …
Today I had a very interesting conundrum that had to get solved quickly. Needless to say, it wasn't resolved quickly because when we needed it we were very rushed, but as soon as the conference call was over and I took a step back I saw the correct …
This tutorial will walk an individual through the steps necessary to join and promote the first Windows Server 2012 domain controller into an Active Directory environment running on Windows Server 2008. Determine the location of the FSMO roles by lo…
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 to another domain controller. Log onto the new domain controller with a user account t…

706 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

Need Help in Real-Time?

Connect with top rated Experts

19 Experts available now in Live!

Get 1:1 Help Now