Hey, Scripting Guy! How Can I Find Expiring Passwords?

Hey, Scripting Guy! How Can I Find Expiring Passwords?

  • Comments 5
  • Likes

Hey, Scripting Guy! Question

Hey, Scripting Guy! The pointy headed boss (PHB) is at it again. He has decided he does not like it when users do not change their password when prompted. As a result, he wants me to produce a report once a week of all the users who will have to change their password next week. Please tell me it can’t be done so I can get him off my back. I looked in Active Directory Domain Services Users and Computers, but I do not see this listed anywhere.

- AM

SpacerHey, Scripting Guy! Answer

Hi AM,

Sometimes it is just better to go with the flow. The astute network administrator can always make things seem to work out for the best when confronted with a PHB. I dare not ask if he wants you to personally call each user and help them in picking out good passwords, and stand by just in case they get it wrong or lock themselves out. Because everyone knows that network administrators really do not do all that much anyway (according to PHB-think), maybe you should suggest this to your boss. Maybe the entire IT department can take each Tuesday—instead of doing useless things like testing patches—to help the users with their passwords. Or maybe not! Sorry, having been there, done that, and worn the scars, I am still a bit sensitive. Why do you think I run for an hour each day? Here is a better suggestion, which I have made before and will make again. Tell the PHB it will take you two days to retrieve the results. Run this script, and then go scuba diving. This view should give you some incentive:

Image of underwater gorgeousness

 

Here is the ExpiringUserPasswords.ps1 script. If you want to leave now and get some dives in before dinner, we'll understand:

$MaxPasswordAge = 30
$userCount = 0
$adsiSearcher = new-object DirectoryServices.DirectorySearcher("LDAP://rootdse")
$adsiSearcher.filter = "objectCategory=user"
$adsiSearcher.findall() | 
Foreach-Object -ErrorAction "silentlycontinue" `
-Begin { "The following users need to set their  password" } `
-Process `
{ 
 $pwdChanged = ([adsi]$_.path).psbase.InvokeGet("PasswordLastChanged")
 If( ((get-date) - $pwdChanged).days -ge $MaxPasswordAge)
   { 
    ([adsi]$_.path).name 
    $userCount ++
   } #end if date
} `
-end { "A total of $userCount users" }

The first thing we do in the ExpiringUserPasswords.ps1 script is create a couple of variables. The first variable is $MaxPasswordAge which we set to 30. You would set this value to whatever your password age policy is for the domain minus the amount of notification you wish to give the users. The next variable is a counter variable we will use to tell us how many users have passwords coming up for renewal. Remember in Windows PowerShell that all variables begin with the dollar sign. This is seen here.

$MaxPasswordAge = 30
$userCount = 0

Now we need to create the DirectoryServices.DirectorySearcher object. You will see the DirectorySearcher class come up again this week. This .NET Framework class specializes in searching Active Directory Domain Services (AD DS). To create the class, we use the New-Object cmdlet to give it the name of the class, and tell it where we wish to connect. We wish to use the LDAP protocol, and connect to rootdse. This allows you to run the script without having to worry about the domain name. We store the returned DirectorySearcher object in the variable called $adsiSearcher. This is shown here:

$adsiSearcher = new-object DirectoryServices.DirectorySearcher("LDAP://rootdse")

We create our filter. This filter is very similar to the ones you would have used in VBScript. As a good exercise, you may want to refer to some of the search AD DS scripts from the Script Repository and adapt some of those search filters to this script. Our filter is nothing fancy; we simply choose the objectCategory attribute from AD DS if the value of the attribute is equal to user. Do not allow any spaces to slip in, or you will regret it. Big time. Trust me. The code is here:

$adsiSearcher.filter = "objectCategory=user"

We then use the findall method from the DirectorySearcher object. We could have used other methods, such as findone, but that is not as exciting as finding all the users. We pipeline the results of the method call as shown here:

$adsiSearcher.findall() |

Now we cheat a little. Remember that magic phrase from VBScript days? You know what I am talking about: On Error Resume Next. There was a reason for it hanging around the top of some scripts (such as they would not run otherwise.) Well, we are not cheating that much. The problem is that when you run this script without the -ErrorAction "silentlycontinue" you are confronted by a bunch of red error messages. They are not related to the script itself, but are related to using the InvokeGet method, which we will get to later. When you try to call the method on an empty attribute, it generates an error.

Anyway, I spent way too much time looking at fish pictures from my dive vacation to the Little Cayman islands, and I did not have time to figure out how to trap the error. I justified it by realizing I need to tell my loyal readers they can specify the -ErrorAction parameter on every single cmdlet. That is right, it is what is called a common parameter. It is commonly found on all the cmdlets. There are actually four levels you can specify for the ErrorAction: continue, silentlyContinue, inquire, and stop. Continue is the default action, which means the script will try to continue running, but it will let you know about the errors. SilentlyContinue is the same as On Error Resume Next. It is the "forgetaboutit" level. Or as my Australian buddies say (hi Brett, Jit, Chris, and Pete), "no worries mate":

Foreach-Object -ErrorAction "silentlycontinue" `

We next specify the -Begin parameter of the Foreach-Object cmdlet. This section of code is only run one time. Here we use this occasion to print out our header message. Note the backtick, which is for line continuation.

-Begin { "The following users need to set their  password" } `

The main part of the code occurs within the -Process section of the Foreach-Object cmdlet. The code here will run once for each item that comes across the pipeline. The object we get back from the DirectorySearcher is a SearchResult object which has a path property. We can use the path property to obtain a DirectoryEntry class. What does this really mean? It means that the results from the search are pretty well useless for us except that we can get the path, and use it to connect to the actual object in AD DS. This is what we are doing with this code: [adsi]$_.path. The $_ is an automatic variable that refers to the current object on the pipeline. Because it is an object, it has methods and properties. It is a SearchResult object. It has a path property. We use the path property and feed it to the [adsi] type accelerator. This will allow us to get a DirectoryEntry class. We use the .psbase to gain access to the base object, which has the InvokeGet method. The InvokeGet method is used to retrieve the PasswordLastChanged property from AD DS. We store this value in the $pwdChanged variable. This is seen here:

-Process `
{ 
 $pwdChanged = ([adsi]$_.path).psbase.InvokeGet("PasswordLastChanged")

Now we use the Get-Date cmdlet to retrieve the current date and time. This actually returns a system.datetime class. We subtract the $pwdChanged datetime object from the current datetime, and select only the days property. If the number of days is greater than or equal to the value we set in the $maxPasswordAge variable, we print out the name of the object and increment the value in the $userCount variable by one. We do this by the convenient (if obscure) syntax of $userCount ++, which means take the value stored in the $userCount variable and increment it by one. This is seen here:

If( ((get-date) - $pwdChanged).days -ge $MaxPasswordAge)
   { 
    ([adsi]$_.path).name 
    $userCount ++
   } #end if date
} `

The total number of users that need to change their password is stored in the $userCount variable. We print this out after everything else is done. The -end parameter of the Foreach-Object cmdlet allows us to run post processing if we wish. This is seen here:

-end { "A total of $userCount users" }

Well, AM, that is about it. See you tomorrow.

Ed Wilson and Craig Liebendorfer, Scripting Guys

Your comment has been posted.   Close
Thank you, your comment requires moderation so it may take a while to appear.   Close
Leave a Comment
  • If i wanted to only run this for users in a certain OU, how could I do that?

    Thanks,

    Ryan

  • THIS IS CRAP ALL I NEED IS TO FIND OUT WHAT MY OLD PASSWORD IS THE ONE I HAVE BEEN USING IS NO LONGER ACCEPTED THUS I CAN NOT CHANGE IT TO A NEW ONE UNLESS I CAN USE THE OLD ONE WHICH THIS STUPID SYSEM WILL NOT ALLOW ME TO DO.  I AM TRYING TO DO A JOB AND I DON'T HAVE TIME TO DO THIS CRAP,.

  • Hi Carol-

    I am sorry that your old password does not work. The point of the article was for a network administrator to run a report that will tell them when certain user passwords are getting ready to expire. As you point out, the script will not help you, but then it was not designed to do that in the first place.

    There is no way that I can write a script that will recover your old (current) password. That would be a HUGE security vulnerability if I could do that. What you will need to do is to ask your system administrator to reset your password.

  • Carol, you're an ungrateful, rude idiot! Next time try to use your brain and remember your own password!

  • Hello,

    this script looks exactly like what i need but i have a few questions.  being new to the scripting world when i changed the max age to 180 (our pwd policy is 6 months) it came back with many names still.  

    the second question i have is is there a way to put these people into a file (CSV or otherwise) where i can email them a general reminder en bulk?

    basically our users get locked out of our system when it comes time to change their passwords (remote users) and i am on the phone a solid 5 hours for about 3 days when this happens and while its my job it is a bit aggravating as most can imagine