Skip to main content

Determine When User Passwords Will Expire with PowerShell

Every once and a while, I take on a project brought in by a member of my PowerShell class.  On the last day of class, I ask people to be ready to work on a “simple” project.  Sometimes the project they bring in is not simple.  If it intrigues me enough, I’ll steer them towards something more skill appropriate and then take on the original project myself.

 

This project involves getting a list of user names that are with in a specified number of days from expiring.  With the exception of connecting to Active Directory to find the MaxPwdAge attribute, this would be simple.  The problem comes when Fine Grain Password Policies come into play.  For this reason, I decided that I had better take this one on.

 

Fine Grain Password Policies allow us to provide different password standards to different users based on their security group membership.  We prioritize the Fine Grain Password Policies (Also known as a Policy Setting Object or PSO) with a Precedence property so if a user is a member of multiple security groups with PSOs applied to them, The highest ranking PSO wins.  This corresponds to the lowest numeric Precedence number.  Also, If a user object is specifically assigned to a PSO, it overrides all security group enforced PSOs. 

 

The first couple segments of codes is simple the PowerShell scripts that I used to set up my test environment.  This environment needs only a single domain controller and all scripts are executed  as an Administrator.

 

Code Example 1: Setting up the PSOs

The code example below will create 3 Fine Grain Password Policies on Windows Server 2008.

# =========================================================
#
Script to set up 3 PSOs.
#
Jason Yoder, MCT
#
Twitter: JasonYoder_MCT
#
FaceBook: MCTExpert
#
Blog: www.MCTExpert.blogspt.com
#
Created for PowerShell class in Columbus, Oh
#
June 22, 2012
#
=========================================================

# Import the Active Directory Module
Import-Module ActiveDirectory

# Get the users credientials
#
$Cred = Get-Credential -Credential "Contoso\Administrator"

# Create the first Fine Grain Password Policy for Group 1.

New-ADFineGrainedPasswordPolicy -Name
"Group1PSO" `
-Precedence
10 `
-ComplexityEnabled
$True `
-Description
"PSO for Group 1" `
-DisplayName
"Group1PSO" `
-LockoutDuration
"0.12:00:00" `
-LockoutObservationWindow
"0.00:15:00" `
-LockoutThreshold
3 `
-MaxPasswordAge
"10.00:00:00" `
-MinPasswordAge
"1.00:00:00" `
-MinPasswordLength
8 `
-PasswordHistoryCount
10 `
-ReversibleEncryptionEnabled
$False

# Create the first Fine Grain Password Policy for Group 2

New-ADFineGrainedPasswordPolicy -Name
"Group2PSO" `
-Precedence
10 `
-ComplexityEnabled
$True `
-Description
"PSO for Group 2" `
-DisplayName
"Group1PSO" `
-LockoutDuration
"0.12:00:00" `
-LockoutObservationWindow
"0.00:15:00" `
-LockoutThreshold
3 `
-MaxPasswordAge
"15.00:00:00" `
-MinPasswordAge
"1.00:00:00" `
-MinPasswordLength
8 `
-PasswordHistoryCount
10 `
-ReversibleEncryptionEnabled
$False

# Create the first Fine Grain Password Policy for Group 2

New-ADFineGrainedPasswordPolicy -Name
"Group3PSO" `
-Precedence
10 `
-ComplexityEnabled
$True `
-Description
"PSO for Group 3" `
-DisplayName
"Group1PSO" `
-LockoutDuration
"0.12:00:00" `
-LockoutObservationWindow
"0.00:15:00" `
-LockoutThreshold
3 `
-MaxPasswordAge
"20.00:00:00" `
-MinPasswordAge
"1.00:00:00" `
-MinPasswordLength
8 `
-PasswordHistoryCount
10 `
-ReversibleEncryptionEnabled
$False
 
The PSOs will help us simulate an environment with multiple PSO assignments to users.
 
Code Example 2: Setting up the Security Groups
PSOs can be assigned to both security groups and individual user objects.  If a user object is assigned to a PSO, then all group assigned PSOs are ignored.
# =========================================================
#
Script to create 3 global security groups
#
Jason Yoder, MCT
#
Twitter: JasonYoder_MCT
#
FaceBook: MCTExpert
#
Blog: www.MCTExpert.blogspt.com
#
Created for PowerShell class in Columbus, Oh
#
June 22, 2012
#
=========================================================

Import-Module activedirectory

New-ADGroup -Name
"Group1" -SamAccountName Group1 `
-GroupCategory Security -DisplayName
"Group 1" `
-GroupScope Global
`
-Path
"CN=Users,DC=Contoso,DC=com" `
-Description
"Group 1 for Password Expiration Test"

New-ADGroup -Name
"Group2" -SamAccountName Group2 `
-GroupCategory Security -DisplayName
"Group 2" `
-GroupScope Global
`
-Path
"CN=Users,DC=Contoso,DC=com" `
-Description
"Group 2 for Password Expiration Test"

New-ADGroup -Name
"Group3" -SamAccountName Group3 `
-GroupCategory Security -DisplayName
"Group 3" `
-GroupScope Global
`
-Path
"CN=Users,DC=Contoso,DC=com" `
-Description
"Group 3 for Password Expiration Test"

 


Code Example 3: Creating User Accounts


To help simulate our environment, we are going to add 500 user accounts.  Depending on the speed of your test environment, this may take a few minutes.  If you are running this in a live environment, take note that these accounts will have a simple password and will be enabled.  Also, this script will add the user account to one or more of the three security groups created earlier.


# =========================================================
#
Script to create 500 User accounts
#
Jason Yoder, MCT
#
Twitter: JasonYoder_MCT
#
FaceBook: MCTExpert
#
Blog: www.MCTExpert.blogspt.com
#
Created for PowerShell class in Columbus, Oh
#
June 22, 2012
#
=========================================================
Import-Module ActiveDirectory

$Count = 0

For ($x = $Count;$X -lt 500;$X++)
{
  
# Create the User Name
   $UserName = "User$x"
   
  
# Create the User Object in Active Directory, give it a
   # password and Enable it.
    New-ADUser -Name$UserName -SamAccountName $UserName `
        -AccountPassword (
ConvertTo-SecureString `
      
-AsPlainText "Pa$$Word1" -Force)`
        -Enabled
$True `
        -DisplayName
"$UserName"
  

  
# Assign the users into Groups.
   # Group assignment is based on whether or not the user
   # name contains a 1, 2, or 3.  The use may be assigned
   # into up to 3 groups, or none at all.  This is by
   # design to simulate multiple Fine Grain Password
   # Policies being applied to the User object.
   If ($UserName -Like "*1*")
    {
        Add-ADGroupMember -Identity Group1
$UserName
    }
   
      
If ($UserName -Like "*2*")
    {
        Add-ADGroupMember -Identity Group2
$UserName
    }
   
      
If ($UserName -Like "*3*")
    {
        Add-ADGroupMember -Identity Group3
$UserName
    }

}


Code Example 4: Adding Security Groups and User Objects to the PSOs.


This code example will add each security group to an individual PSO.  It will also set some user objects to be directly assigned to a PSO.  To verify that the user setting overrides the group settings.  Some of these users will be set in a lower precedence PSO as compared to their group membership.

# =========================================================
#
Script to assign users and groups to PSOs.
#
Jason Yoder, MCT
#
Twitter: JasonYoder_MCT
#
FaceBook: MCTExpert
#
Blog: www.MCTExpert.blogspt.com
#
Created for PowerShell class in Columbus, Oh
#
June 22, 2012
#
=========================================================
#
Import the Active Directory Module
Import-Module ActiveDirectory

# Add Groups to the PSO
Add-ADFineGrainedPasswordPolicySubject -Identity Group1PSO `
-Subjects Group1
Add-ADFineGrainedPasswordPolicySubject -Identity Group2PSO
`
-Subjects Group2
Add-ADFineGrainedPasswordPolicySubject -Identity Group3PSO
`
-Subjects Group3

# Add individual users to groups to take into account user
#
objects that are added to PSOs as opposed to being added
#
as part of a security group.

# Add uses to a higher Precedence PSO
Get-ADUser -Filter `
'Name -like "User*23" -or Name -like "User*32"' |
Add-ADFineGrainedPasswordPolicySubject -Identity Group1PSO

# Add Users to a Lower precedence PSO
Get-ADUser -Filter `
'Name -like "User*21" -or Name -like "User*12"' |
Add-ADFineGrainedPasswordPolicySubject -Identity Group3PSO

 


Now our test environment is built.


Code Example 5: Finding Whose Password is About to Expire


This block of code is designed to be dot sourced into your PowerShell session. Once there, you can execute the cmdlet Get-PasswordExpirations including calling its help file. This help file will provide instructions on how to use this  cmdlet.


 


Function Get-MaxPwdAge
{
# Get the Domain information
$Domain = Get-ADDomain

# Get the domain root
$DNSRoot = $Domain.DNSRoot

# Get the Domain Distinguished Name.
$DistinguishedName = $Domain.DistinguishedName

# Connect to Active Directory
$Connection = "LDAP://"+$DistinguishedName
$AD = [ADSI]$Connection

# Extract the Maximum Password Age from AD and convert it to days.
$MaxPwdAge = -($AD.ConvertLargeIntegerToInt64($AD.MaxPwdAge.Value))/(600000000 * 1440)

Write-Output $MaxPwdAge
}


<#
.SYNOPSIS
Confirms if a module is available.

.DESCRIPTION
Confirms if the provided parameter is available on
the local client.

.PARAMETER ModuleName
The name of the module who?s presence is being checked.

.EXAMPLE
Confirm-Module ActiveDirectory

Checks to see if the ActiveDirectory module is
present on the local machine
Returns True is present and False if not.

.OUTPUTS
Boolean

.Link
Get-Module
#>

Function Confirm-Module
{
Param ($ModuleName = $(Throw "You need to provide a module name."))
# Place the name of the module from Get-Module into
# the variable $Data
$Data = (Get-Module -ListAvailable -Name $ModuleName).name

# If the contents of $Data is equal to the variable
# $ModuleName, the module is present, return
# True. If not, return $False.
If ($Data -eq $ModuleName){Return $True}
Else {Return $False}
}


Function Get-PasswordDayDiff
{
param (
$Date1

)
$Date2 = Get-Date
if ($Date2 -gt $Date1)
{
$DDiff = $Date2 - $Date1
}
Else
{
$DDiff = $Date1 - $Date2
}
Write-Output $DDiff



}

<#
.SYNOPSIS
Displays a list of users whos accounts are within a number of days
of requiring a reset.

.DESCRIPTION
This cmdlet can be used to help determine which user objects within
Active Directory are within a specified number of days from requiring
a new password.

.PARAMETER NumOfDays
This is the number of days that the user wants to see if any account
passwords will expire.

.PARAMETER All
Shows all the properties from this cmdlet

.PARAMETER ShowDaysSinceChange
Shows the number of days since the user changed their password.

.PARAMETER ShowDaysTillChange
Shows the number of days until the users password will need
to be reset

.PARAMETER ShowLastPasswordReset
Shows the date and time of the last password reset for the
user object.

.PARAMETER ShowMaxAge
Shows the maximum passowrd age allowed for a user object.

.PARAMETER ShowPrecedence
Shows the Precedence level of the Fine Grain Password Policy
that is being enforced on the user account. A value of 9999
if used internally by this cmdlet to denote the Default Domain
Policy is being used to set the maximum password age.

.EXAMPLE
PS C:\> Get-PasswordExpirations 10 | FT -AutoSize

UserName WithinRange
-------- -----------
User1 True
User10 True
User11 True
User14 True
User15 True
User16 True

Description
-----------
Displays a list of all user accoutns whose passwords will be expiring withing 10 days.

.EXAMPLE
PS C:\> Get-PasswordExpirations 10 -AllUsers | Select-Object -First 100 | FT -AutoSize

UserName WithinRange
-------- -----------
User0 False
User1 True
User3 False
User4 False
User5 False
User6 False
User7 False
User8 False
User9 False
User10 True

Description
-----------
Displays the first 10 users objects that include both those that will have
passwords expiring withing 10 days and those that do not.

.EXAMPLE
PS C:\> Get-PasswordExpirations 10 -AllUsers -ShowDaysTillChange |
Where-Object {$_.DaysTillchange -le 16} |
FT -AutoSize

UserName DaysTillChange WithinRange
-------- -------------- -----------
User1 10 True
User10 10 True
User11 10 True
User14 10 True
User15 10 True
User16 10 True
User17 10 True
User18 10 True
User19 10 True
User20 15 False
User22 15 False


Description
-----------
Shows a list of all users and the number of days until the
password expires. The WithRange property is looking for
accounts that will exprie in the next 10 days.
#>

Function Get-PasswordExpirations
{
# =========================================================
#
Script to create 3 global security groups
#
Jason Yoder, MCT
#
Twitter: JasonYoder_MCT
#
FaceBook: MCTExpert
#
Blog: www.MCTExpert.blogspt.com
#
Created for PowerShell class in Columbus, Oh
#
June 22, 2012
#
=========================================================
param (
[CmdletBinding()]

[parameter(Mandatory
=$true)][int]$NumOfDays,
[
switch]$All,
[
switch]$AllUsers,
[
switch]$ShowMaxAge,
[
switch]$ShowPrecedence,
[
switch]$ShowLastPasswordReset,
[
switch]$ShowDaysSinceChange,
[
switch]$ShowDaysTillChange
)

# Test to see if the ActiveDirectory module is
# available on this client.
# Exit the script if it is not.
If ((Confirm-Module ActiveDirectory) -eq $False)
{
Write-Host "ActiveDiretory Module Not Present" `
-ForegroundColor Red -BackgroundColor Black
BREAK
}

# Load the cmdlets needed from the Active Directory
# module Present for backword compatability with
# PowerShell V2.
Import-Module ActiveDirectory -Cmdlet Get-ADUser, `
Get-ADFineGrainedPasswordPolicy

# Get the Maximum Password Age from Active Directory.
# Note: This calls a supporting function.
$MaxPwdAge = Get-MaxPwdAge

# Extract the PSO information that will be needed.
$PSOInfo = Get-ADFineGrainedPasswordPolicy -Filter * |
Select-Object -Property Name, AppliesTo, `
MaxPasswordAge, Precedence

# Extract the user information from Active Directory
$UserInfo = Get-ADUser -Filter * -Properties DisplayName, `
DistinguishedName, MemberOf, PasswordLastSet


<#
Rules for determining the current password policy
1. If the user is not assigned to a PSO, then use the
Default Domain Policy.
2. If user is part of a Security Group that is assied
a PSO, use that PSO.
2a. If the user is assigned to multiple POSs, use
the higher precedence PSO.
3. If the user object is specifically assigned to a
PSO, override anny Group PSO.
3a. If the user object is assigned to multipl PSOs,
use the higher precedence PSO.
#>

# Create a dynamic array to hold the custom object to
# be sent to the pipeline.
$PasswordObj = @()

# Loop through the user objects.
ForEach ($User in $UserInfo)
{

# Test to see if the Password has ever been set.
# If not, ignor this account.

If ($User.PasswordLastSet -NE $null)
{

# Set the starting precedence number.
# This is used to determine which PSO to use.
$PrecedenceNumber = 9999

# Flag to denote that a PSO has a User object
# specified in it. This will override and
# Group set PSO.
$UserSetPSO = $False

# PSOFound flag is used to seperate the users
# who have a PSO applied to them and those
# that do not.
$PSOFound = $False
$PSOMaxPwdAge = $MaxPwdAge



# Loop through each PSO
ForEach ($PSO in $PSOInfo)
{
# Create the object to store the data.
$Obj = New-Object PSObject

# Get the Membership information of the
# user.
$MemberOf = $User.MemberOf

# Get the AppliesTo information for the
# current PSO
$AppliesTo = $PSO.AppliesTo

# Loop through the MemberOf information
ForEach ($Member in $MemberOf)
{

# Loop through the AppliesTo
# information
ForEach ($Applies in $AppliesTo)
{

# Compare the Groups of the user
# and PSO only if the User object
# has not previously found in
# a PSO AppliesTo property.
If (($Member -eq $Applies) -and ($PSOInfo.Precedence -lt $PrecedenceNumber) -and ($UserSetPSO -eq $False))
{
$PrecedenceNumber = $PSO.Precedence
$PSOFound = $True
$PSOMaxPwdAge = ($PSO.MaxPasswordAge).days }

# Compare the User Object to the
# PSO AppliesTo property. If they match,
# Set the $UserSetPSO flag to true.
# Then reset the $PrecedenceNumber number
# and apply the new Presedence number to
# the current User.

If ($User.DistinguishedName -eq $Applies)
{
$UserSetPS = $True
$PrecedenceNumber = 9999
If ($PSOInfo.Precedence -lt $PrecedenceNumber)
{
$PrecedenceNumber = $PSO.Precedence
$PSOFound = $True
$PSOMaxPwdAge = ($PSO.MaxPasswordAge).days
}

}

}
}

}
# End: ForEach ($PSO in $PSOInfo)

# Create the object for each instance.
$Obj = New-Object PSObject

# Add the user name to the Object.
$Obj | Add-Member -MemberType NoteProperty -Name UserName -Value $User.DisplayName

# Add the number of days since the password has been changed to the object.
$DDiff = (Get-PasswordDayDiff $User.PasswordLastSet).Days
If ($ShowDaysSinceChange -or $all)
{

$Obj | Add-Member -MemberType NoteProperty -Name DaysSinceChange -Value $DDiff
}

# Determine if the password reset is within the parameter specified
# by the user.

If (($PrecedenceNumber -eq 9999) -and ($DDiff -lt $MaxPwdAge))
{
$DaysTillChange = ($MaxPwdAge - $DDiff)
If ($ShowDaysTillChange -or $all)
{
$Obj | Add-Member -MemberType NoteProperty -Name DaysTillChange -Value $DaysTillChange
}
}
Else
{
$DaysTillChange = ($PSOMaxPwdAge - $DDiff)
If ($ShowDaysTillChange -or $all)
{
$Obj | Add-Member -MemberType NoteProperty -Name DaysTillChange -Value $DaysTillChange
}
}


# Set to True if the password is within the range specified by the user.
If ($DaysTillChange -le $NumOfDays)
{
$Obj | Add-Member -MemberType NoteProperty -Name WithinRange -Value $True
}
Else
{
$Obj | Add-Member -MemberType NoteProperty -Name WithinRange -Value $False
}


# Display the maximum number of days that a users password can be used.
# before needing to be reset.
If ($ShowMaxAge -or $all)
{
$Obj | Add-Member -MemberType NoteProperty -Name MaxPwdAge -Value $PSOMaxPwdAge
}

# Add the precedence value of the PSO. Note: a vaule of 9999 is the
# default domain policy.
If ($ShowPrecedence -or $all)
{
$Obj | Add-Member -MemberType NoteProperty -Name PrecedenceNum -Value $PrecedenceNumber
}

# Add the date of the last password reset.
If ($ShowLastPasswordReset -or $all)
{
$Obj | Add-Member -MemberType NoteProperty -Name PWDLastSet -Value $User.PasswordLastSet
}

# Commit the instance object to the output if the account is within the range.
# Also write the data for users outside the range is the -AllUsers
# switch is $True.
If ($DaysTillChange -le $NumOfDays)
{
$PasswordObj += $Obj
}
ElseIf (($DaysTillChange -gt $NumOfDays) -and $AllUsers)
{
$PasswordObj += $Obj
}
}
# End: If ($User.PasswordLastSet -NE $null)
} # End: ForEach ($User in $UserInfo)

#Write the data to the pipeline.
Write-Output $PasswordObj
}

 


 

Comments

Ziemek Borowski said…
This comment has been removed by the author.
Hi Ziemek. You are welcome to modularize all you want. Go for it my friend.

Popular posts from this blog

Adding a Comment to a GPO with PowerShell

As I'm writing this article, I'm also writing a customization for a PowerShell course I'm teaching next week in Phoenix.  This customization deals with Group Policy and PowerShell.  For those of you who attend my classes may already know this, but I sit their and try to ask the questions to myself that others may ask as I present the material.  I finished up my customization a few hours ago and then I realized that I did not add in how to put a comment on a GPO.  This is a feature that many Group Policy Administrators may not be aware of. This past summer I attended a presentation at TechEd on Group Policy.  One organization in the crowd had over 5,000 Group Policies.  In an environment like that, the comment section can be priceless.  I always like to write in the comment section why I created the policy so I know its purpose next week after I've completed 50 other tasks and can't remember what I did 5 minutes ago. In the Group Policy module for PowerShell V3, th

Return duplicate values from a collection with PowerShell

If you have a collection of objects and you want to remove any duplicate items, it is fairly simple. # Create a collection with duplicate values $Set1 = 1 , 1 , 2 , 2 , 3 , 4 , 5 , 6 , 7 , 1 , 2   # Remove the duplicate values. $Set1 | Select-Object -Unique 1 2 3 4 5 6 7 What if you want only the duplicate values and nothing else? # Create a collection with duplicate values $Set1 = 1 , 1 , 2 , 2 , 3 , 4 , 5 , 6 , 7 , 1 , 2   #Create a second collection with duplicate values removed. $Set2 = $Set1 | Select-Object -Unique   # Return only the duplicate values. ( Compare-Object -ReferenceObject $Set2 -DifferenceObject $Set1 ) . InputObject | Select-Object – Unique 1 2 This works with objects as well as numbers.  The first command creates a collection with 2 duplicates of both 1 and 2.   The second command creates another collection with the duplicates filtered out.  The Compare-Object cmdlet will first find items that are diffe

How to list all the AD LDS instances on a server

AD LDS allows you to provide directory services to applications that are free of the confines of Active Directory.  To list all the AD LDS instances on a server, follow this procedure: Log into the server in question Open a command prompt. Type dsdbutil and press Enter Type List Instances and press Enter . You will receive a list of the instance name, both the LDAP and SSL port numbers, the location of the database, and its status.