Bulk change date for user PW resets

Active Directory Functionality level is at 2012 R.  I need to do two things

1. Run a report for the date all users will need to change their PW

2.  Take Users that are bulk together on one day and spread them out.  So if I have 300 users set to reset their pw on same day I need to spread them out a bit.
Who is Participating?
Dustin SaundersConnect With a Mentor Director of OperationsCommented:
I saw no one had taken up the gig yet and your deadline is tomorrow.  I don't have time to see a gig through to completion, but here is some code to get you going.  I've tested small scale and it seems to be working fine, but set up a test environment or change $searchBase to small OU to test.

The variable $maxExtended is the total number of days you are willing to push out the expiration.  For each day here, you need to create a group called "x Day Expire" (i.e. 1 Day Expire, 2 Day Expire).  Then in ADAC, set a fine grained password policy, no min expiration and the expiration = to the number of days in X (see example for 2 days expiration).

$maxPerDay is the acceptable max per day that can have an expiration.

The script will get everyone's expiration day in the year (i.e. 1-9-2016 = 9, 2-1-2016 = 32) and look ahead for each $maxExtended days to distribute the resets.  If within the acceptable limit of days to push out there isn't room, it will retain it's default expiration date (recommend doing this so people don't have resets that potentially never expire).

Then for each move, we add the user into "x Day Expire" to get the password policy and then reset, and remove from the group.

I don't usually sink this much time into an EE problem, but you can take this and play with it (or open another question if there is an issue).

Import-Module ActiveDirectory

$searchBase = "DC=yourdomain,DC=local"  #root OU to search for users.
$maxPerDay = 20  #acceptable number of passwords to expire the same day.
$maxExtended = 7  #number of days maximum to push back expiration.

function ResetPasswordExpiry($days, $user)
    $ADUser = Get-ADUser -Filter {sAMAccountName -eq $user} -Properties pwdLastSet
    $expGroup = "$days Day Expire"
    Write-Host "Moving EXP for $user to $expGroup"
    Add-ADGroupMember $expGroup -Member $user -Confirm:$false
    Set-ADUser $ADUser -Replace @{pwdLastSet=0} -whatif
    Set-ADUser $ADUser -Replace @{pwdLastSet=-1} -whatif
    Remove-ADGroupMember $expGroup -Member $user -Confirm:$false

function ExpirationTable
    $dt = New-Object System.Data.DataTable
    $c1 = New-Object System.Data.DataColumn 'distinguishedName',([string])
    $c2 = New-Object System.Data.DataColumn 'sAMAccountName',([string])
    $c3 = New-Object System.Data.DataColumn 'expDOY',([int])
    return, $dt

function AddRow ($exp, $dn, $sam)
    $row = $expTable.NewRow()
    $row.sAMAccountName = $sam
    $row.distinguishedName = $dn
    $row.expDOY = $exp

$expTable = ExpirationTable

$expiringUsers = Get-ADUser -Properties msDS-UserPasswordExpiryTimeComputed -Filter * -SearchBase $searchBase | where {$_.Enabled -eq "True"}

foreach ($user in $expiringUsers)
    $expDate = [datetime]::FromFileTime($user."msDS-UserPasswordExpiryTimeComputed")
    AddRow $expDate.DayOfYear $user.DistinguishedName $user.SamAccountName

$expTable.DefaultView.Sort = "expDOY"
$expTable = $expTable.DefaultView.ToTable()

$doy = 1

while ($doy -lt 365)
    $thisExpires = $expTable.Select("expDOY = $doy")  #get expirations for this Day of Year

    if ($thisExpires.Length -gt $maxPerDay)
        $overload = $thisExpires.Length - $maxPerDay
        $lookrow = $maxPerDay + 1
        $nextDay = $doy + 1
        while ($overload -gt 0 -and $nextDay -le ($doy + $maxExtended))
            $expNextDay = $expTable.Select("expDOY = $nextDay")
            if ($expNextDay.Length -lt $maxPerDay)
                $available = $maxPerDay - $expNextDay.Length
                while ($available -gt 0)
                    $r = $thisExpires[$lookRow]
                    Write-Host $r.sAMAccountName
                    Write-Host $lookrow
                    ResetPasswordExpiry ($nextDay - $doy) $r.sAMAccountName
                    $r.expDOY = $nextDay
                    catch {
                    Write-Host "Row $lookRow was empty"
                    $available = $available - 1
                    $overload = $overload - 1


Open in new window

Note that due to this:
    Set-ADUser $ADUser -Replace @{pwdLastSet=0} -whatif
    Set-ADUser $ADUser -Replace @{pwdLastSet=-1} -whatif

Open in new window

it won't actually make any changes until you remove -whatif.  If you have issues you could also start a new gig to work on this existing code.
Dustin SaundersDirector of OperationsCommented:
Tech support getting flooded on reset day?  :)

What do you consider 'bulk'?
Twhite0909Author Commented:
LOL Ya helpdesk got absolutely blown up w 400 users on day.  I still say 100 is bulk.  so I wanna generate a powershell that pipes to csv for Users reset days.  Filter by reset day so I can see anyone over 100 then take that group/groups and somehow tell them disperse......?
SMB Security Just Got a Layer Stronger

WatchGuard acquires Percipient Networks to extend protection to the DNS layer, further increasing the value of Total Security Suite.  Learn more about what this means for you and how you can improve your security with WatchGuard today!

Dustin SaundersDirector of OperationsCommented:
Hmm..  this is trickier than I expected, but I'll work on something this afternoon.
Twhite0909Author Commented:
Dude you are the man Dustin! LOL Now I already have something that shows me the list of users with expiry date which is

#Get-ADUser -SearchBase "OU=Employees,OU=Birch,DC=birch,DC=com" -filter {Enabled -eq $True -and PasswordNeverExpires -eq $False} –Properties “DisplayName”, “msDS-UserPasswordExpiryTimeComputed” |

#Select-Object -Property “Displayname”,@{Name=“ExpiryDate”;Expression={[datetime]::FromFileTime($_.“msDS-UserPasswordExpiryTimeComputed”)}} | Export-Csv

That worked great and I found our helpdesk exaggerates greatly bc we do not have hundreds of people expiring in 1 day.  We have blocks of 20,30,40 who all expire on the same day.  However management still wants us to get that number down.  So do you think you have anything that that:

A.  Finds all Users in my OU, lists expiry date

and then

B.  adds on top of their current expiry date to be a randomized number between 30-60 days

Does that make sense
Dustin SaundersDirector of OperationsCommented:
This expert suggested creating a Gigs project.
How many grand total users?  I don't think a random bump of 30-60 days is the way to go because potentially some users could get randomly reassigned expiry date and never have to reset password (making the reason for the expiry obsolete).

I think the idea would need to be something like:
Max expiry = 20
Find a day that is bad (40 expiry) or a weekend (40 on sat, 40 on sun).
Take the total number of users to be redistributed and crawl out each day until you can reassign them into days in the future keeping the expiry threshold down under 20.

40 expire Monday, 17 expire Tuesday, 15 expire Wed, 5 expire Thu.

Take the total on Monday and leave 20.  Then assign 3 users to Tuesday.  Then assign 5 users to Wed.  Then assign the remaining 12 to Thursday.

20 expire Monday, 20 expire Tuesday, 20 expire Wed, 17 expire Thu.

You could do that once per week.  Then people get pushed ahead, but not forgotten.  I can help with the code to set the password change date but the whole script (if done as above, which is what I'd recommend) is probably going to end up being an hour or so of work so you might consider posting a gig.

When I get a chance to test the pwdLastSet mod code I'll post that though.
David Johnson, CD, MVPOwnerCommented:
You can't change the expiry date.  The days that a password is valid is set in the default domain policy. You can use fine grained password policies and separate OU's to change the time period/complexity requirements.
	Create a CSV and HTML Report on Expiring Passwords
	This script will create a Password Expiration report.  It creates it in 2 formats,
	CSV and HTML.  Both are then emailed to the specified user.
	Make sure to edit and change the PARAM section to match your environment
	Specify the path where you want to save the CSV report.  The script does not save
	the HTML report, but emails it as the body of the email.
	Tell the script who the script is coming "from".
	Tell the script where to send the email
	This needs to be the IP address or name of your SMTP relay server.
	CSV:	ExpirationReport.csv in the $Path location
	Email:	HTML version of the same report in the body of the email.  Also attaches
			the CSV to the email.
	Accepts all defaults as defined in the PARAM section
	.\Report-PasswordExpiration.ps1 -Path d:\myreports -From script@yourdomain.com -To Administrator@yourdomain.com -SMTPServer
	Runs the report using D:\myreports as the path to save the CSV report.  Email will be sent
	from "script@yourdomain.com" and sent to "Administrator@yourdomain.com" using
	as the SMTP relay server.
	Script:				Report-PasswordExpiration.ps1
	Author:				Martin Pugh
	Function Author:	M. Ali
	Webpage:			www.thesurlyadmin.com
	Twitter:			@thesurlyadm1n
	Spiceworks:			Martin9700
        1.01            Added loading of RSAT tools (if they're installed)
		1.0				Initial Version
	Source code:		
Param (
	[string]$Path = "c:\scripts",
	[string]$From = "admin@yourdomain.com",
	[string]$To = "you@yourdomain.com",
	[string]$SMTPServer = "SMTPServerName"

Function Get-XADUserPasswordExpirationDate() {
	# Function written by M.Ali
	# http://blogs.msdn.com/b/adpowershell/archive/2010/02/26/find-out-when-your-password-expires.aspx
	# Modified by Martin Pugh
    Param (
		[Parameter(Mandatory=$true,  Position=0,  ValueFromPipeline=$true, HelpMessage="Identity of the Account")]
		[Object] $accountObj

        If ($accountObj.PasswordExpired) 
		{	Return "Expired"
		{	If ($accountObj.PasswordNeverExpires) 
			{	Return "Password set to never expire"
			{	$passwordSetDate = $accountObj.PasswordLastSet
                If ($passwordSetDate -eq $null) 
				{	Return "Password has never been set"
				{	$maxPasswordAgeTimeSpan = $null
                    $dfl = (get-addomain).DomainMode
                    If ($dfl -ge 3) 
					{	## Greater than Windows2008 domain functional level
                        $accountFGPP = Get-ADUserResultantPasswordPolicy $accountObj
                        If ($accountFGPP -ne $null) 
						{	$maxPasswordAgeTimeSpan = $accountFGPP.MaxPasswordAge
						{	$maxPasswordAgeTimeSpan = (Get-ADDefaultDomainPasswordPolicy).MaxPasswordAge
					{	$maxPasswordAgeTimeSpan = (Get-ADDefaultDomainPasswordPolicy).MaxPasswordAge
                    If ($maxPasswordAgeTimeSpan -eq $null -or $maxPasswordAgeTimeSpan.TotalMilliseconds -eq 0) 
					{	Return "MaxPasswordAge is not set for the domain or is set to zero!"
					{	Return ($passwordSetDate + $maxPasswordAgeTimeSpan)

Try { Import-Module ActiveDirectory -ErrorAction Stop }
Catch { Write-Host "Unable to load Active Directory module, is RSAT installed?" -ForegroundColor Red; Exit }

$Result = @()
$Users = Get-ADUser -Filter * -Properties GivenName,sn,PasswordExpired,PasswordLastSet,PasswordneverExpires
ForEach ($User in $Users)
{	$Result += New-Object PSObject -Property @{
		'Last Name' = $User.sn
		'First Name' = $User.GivenName
		UserName = $User.SamAccountName
		Expiration = $($User | Get-XADUserPasswordExpirationDate)
$Result = $Result | Select 'Last Name','First Name',UserName,Expiration | Sort 'Last Name'

#Produce a CSV
$Result | Export-Csv $path\ExpirationReport.csv -NoTypeInformation

#Send HTML Email
$Header = @"
TABLE {border-width: 1px;border-style: solid;border-color: black;border-collapse: collapse;}
TD {border-width: 1px;padding: 3px;border-style: solid;border-color: black;}
$splat = @{
	From = $From
	To = $To
	SMTPServer = $SMTPServer
	Subject = "Password Expiration Report"
$Body = $Result | ConvertTo-Html -Head $Header | Out-String
Send-MailMessage @splat -Body $Body -BodyAsHTML -Attachments $Path\ExpirationReport.csv

Open in new window

Dustin SaundersDirector of OperationsCommented:
Looks like David is correct, that attribute can't be modified to anything other than 0 or -1 by the system (my plan was to take the default domain policy and set it to the appropriate pwsLastSet after math from there).

That doesn't mean you're SOL entirely, because you can set/reset the expiration meaning you can manually reset the expiry then have a database file that gets parsed each day and expires then at a later stamped time.

Route 2 would use precedented password policies in ADAC and then have the script reset the passwords and temporarily put them in an x days expire group so when you reset you set the lifespan.  So, i.e. if they get pushed out 4 days, put them in a 4 day expire group and then reset the pwd expiration so it pulls 4 days.  They'd immediately get pulled out of that group so when the actual reset day occurs they're back in the normal policy window.

But to have the whole thing created is going to fall into the realm of gigs (it's a bit of a project), would probably take a couple of hours.  But if it's a huge problem someone would probably quote it at like $100ish (I think it'd take about 2 hours to write and test).
Twhite0909Author Commented:

Thank you both very much for your help and suggestions.  I appreciate it very much! I will create a gigs project to get this done.
Twhite0909Author Commented:
if i could give more than an A I would.  Thanks alot for helping with this and spending so much of your time on it!
Dustin SaundersDirector of OperationsCommented:
No worries, good luck with this project!
Question has a verified solution.

Are you are experiencing a similar issue? Get a personalized answer when you ask a related question.

Have a better answer? Share it in a comment.

All Courses

From novice to tech pro — start learning today.