Tuesday, March 20, 2012

Password Expiration Email Reminders

Below is a PowerShell script that will email password change reminders to users when their password is about to expire.  It uses built in windows functionality and doesn't require any third party software.  It's fully customizable with the ability to easily set email intervals.

The default email schedule will email users at 30 days, 15 days, 7 days, 5 days, 3 days, 2 days, 1 day and the day of password expiration.  If the user changes their password they will stop receiving email notifications until it becomes time to change their password again.

The script will ignore disabled user accounts and accounts that have the password never expires attribute set.  If the user has an associated email address in active directory it will send the reminder to that email address.  If the user does not have an associated email address an email will be sent to the address specified by the $adminEmail variable.  Thus allowing the admin to be proactive.  The script will also email the $adminEmail if a user's account has expired.  Again, so that the admin can be proactive.  The $adminEmail will receive only one email summary to avoid inbox clutter.  The summary will also show user's that are required to change their passwords but have not done so yet.  There's usually a good reason they're not logging on.  Usually these types of accounts were provision for an employee but that employee never started.

You will need an SMTP server that doesn't require authentication to send out the emails.  You will also need the active directory modules in order to run the script.  Information regarding that can be found here: http://technet.microsoft.com/en-us/library/dd378937(WS.10).aspx

In a nutshell, if you're running the script on a domain controller you should be set.  If you want to run it on a member server then execute the following PowerShell commands:

Import-Module ServerManager
Add-WindowsFeature RSAT-AD-PowerShell

You should create a scheduled task to run the script shortly after midnight each night.  Before using it, be sure to set the five configuration variables in the Configurable Settings section at the top of the script.   

Note: The script is just using the default domain password policy but if you're using the AD DS Fine-Grained Password Policies you should be able to modify the script fairly easily.  You'll just have to invoke the Get-ADUserResultantPasswordPolicy commandlet while iterating through the user objects.  Also note that all time is done with GMT and time zones are not taken into account.  This shouldn't be an issue unless your enterprise spans the globe.  If you're enterprise only spans a few contiguous time zones, have the script run at midnight in the time zone that's closest to GMT.

You can download the script here or see below.

CODE:

#Create a nightly scheduled task that runs shortly after midnight and execute this script to generate password expiration email reminders

Import-Module ActiveDirectory

#----- Configurable Settings ------------------------------------------------------------------------------------------------------------------------

# Set the $warningInterval array to change when password expiration email reminders go out.
# The default below will email users at 30 days, 15 days, 7 days, 5 days, 3 days, 2 days, 1 day and the day of.
$warningIntervals = 30,15,7,5,3,2,1,0


# Set the $adminEmail to the email address that will receive password expiration reminders if the user does not have an associated mail address in AD
$adminEmail = 'admin@domain.com'


# Set the $fromEmail to the email address that will be used to send out password expiration reminders
$fromEmail = 'from@domain.com'


# Set the SMTP server used for sending out password expiration reminders, no authentication is used
$smtpServer = "smtp.domain.com"


# If $True, a summary will always be sent to $adminEmail. If $False, a summary will only be sent when there are user accounts that need attention.
$alwaysSendAdminSummary = $False

#----- End Configurable Settings -------------------------------------------------------------------------------------------------------------------


#Constant used to determine if password never expires flag is set
$ADS_UF_DONT_EXPIRE_PASSWD = 0x00010000

#Constant used to determine if user must change password at next login
$REQUIRED_PASSWORD_CHANGE_LASTSET = 0

$CurrentDate = [datetime]::Now.Date
$adminEmailContent = ""

$DefaultDomainPasswordPoliy = Get-ADDefaultDomainPasswordPolicy 
$smtp = new-object Net.Mail.SmtpClient($smtpServer)

function GetDaysToExpire([datetime] $expireDate)
{
    $date =  New-TimeSpan $CurrentDate $expireDate
    return $date.Days
}

function GetPasswordExpireDate($user)
{
    return [datetime]::FromFileTimeUTC($user.pwdLastSet+$DefaultDomainPasswordPoliy.MaxPasswordAge.Ticks)
}

function IsInWarningIntervals([int] $daysToExpire)
{
    foreach( $interval in $warningIntervals )
    {
       if ( $daysToExpire -eq $interval )
       {
        return $True
       }
    }
    return $False
}

function EmailUser($user)
{
    $pwdExpires = GetPasswordExpireDate $user
    $daysToExpire = GetDaysToExpire $pwdExpires 

    if( $daysToExpire -eq 0 )
    {
        $emailContent = $user.DisplayName + ", your password will expire today at " + $pwdExpires.ToShortTimeString() + " GMT"
    }
    else
    {
        $emailContent = $user.DisplayName + ", your password will expire in " + $daysToExpire + " days."
    }
    $smtp.Send($fromEmail, $user.mail, "Password Expiration Reminder", $emailContent)
}

function EmailAdmin($content) 
{
    if( [string]::IsNullOrEmpty($content) -and $alwaysSendAdminSummary )
    {
        $content = "There are no user's with expired passwords or users that need to change their password."
    }
    if( [string]::IsNullOrEmpty($content) -ne $True )
    {
        $smtp.Send($fromEmail, $adminEmail, "Password Expiration Summary", $content)
    }
}

function AppendAdminEmailNoMail($user)
{
    $pwdExpires = GetPasswordExpireDate $user
    $daysToExpire = GetDaysToExpire $pwdExpires 

    return "The password for account """ + $user.samAccountName + """ will expire in " + $daysToExpire + " days at " + $pwdExpires + " and there is no associated email address to send a notification to" + [System.Environment]::NewLine + [System.Environment]::NewLine
}

function AppendAdminEmailExpiredAccount($user)
{
    $pwdExpires = GetPasswordExpireDate $user
    $daysToExpire = GetDaysToExpire $pwdExpires 

    if( $user.pwdLastSet -eq 0 )
    {
        return "The user account """ + $user.samAccountName + """ is set to require a password change at next logon and the user has not yet changed it" + [System.Environment]::NewLine + [System.Environment]::NewLine
    }
    else
    {
        return "The password for user account """ + $user.samAccountName + """ expired " + $daysToExpire + " days ago on " + $pwdExpires + [System.Environment]::NewLine + [System.Environment]::NewLine
    }
}


#get all users
$users = Get-AdUser -Filter * -Properties userAccountControl, pwdLastSet, userprincipalname, mail, DisplayName, samAccountName, accountExpires, enabled  #|
# FT -Property @{Label="UAC";Expression={"0x{0:x}" -f $_.userAccountControl}}, userAccountControl, pwdLastSet, @{Label="pwdLastChanged";Expression={[datetime]::FromFileTimeUTC($_.pwdLastSet)}}, @{Label="pwdExpires";Expression={[datetime]::FromFileTimeUTC($_.pwdLastSet+$DefaultDomainPasswordPoliy.MaxPasswordAge.Ticks)}}, userprincipalname, mail, DisplayName, samAccountName, accountExpires, enabled
#$users

foreach( $user in $users ) 
{  
    #if account is enabled and password never expire flag does not exist, then process user
    if ( ($user.enabled -eq $True) -and (($user.userAccountControl -band $ADS_UF_DONT_EXPIRE_PASSWD) -eq 0) ) 
    {        
        $pwdExpires = GetPasswordExpireDate $user 
        $daysToExpire = GetDaysToExpire $pwdExpires 

        #if day falls on warning interval
        if( IsInWarningIntervals $daysToExpire )
        {
            #if mail attribute is not found in AD, add to admin email
            if ( [string]::IsNullOrEmpty($user.mail) ) 
            {
                $adminEmailContent += AppendAdminEmailNoMail $user
            }
            #otherwise email user
            else
            {
                EmailUser $user     
            }
        }

        #if days to expire is negative, password has expired. add to admin email
        if( $daysToExpire -lt 0 )
        {
            $adminEmailContent += AppendAdminEmailExpiredAccount  $user
        }
    }
}

EmailAdmin $adminEmailContent

16 comments:

  1. Hi,
    Great script, however I only seem to get the adminemail with list of all users attached. The end user (I have given it a mail value) never seems to get the email?
    Any suggestions?

    ReplyDelete
  2. Where/how did you give the user a mail value?

    ReplyDelete
  3. Hi Scott!
    Totally agree with Geraint, super script. Im having the same issue he had; im getting the admin email without users getting the alert mail. I likely broke it myself, however we dont have a AD sandbox for testing, so I couldnt kick out twenty tests to my company while I tooled with it.

    The "get-user -filter *" I modified to "get-user -filter * -searchbase "OU=testing,OU=users,DC=Domain,DC=local""

    Was this enough to break your engine and what do you think would be the best path to repair it?

    ReplyDelete
    Replies
    1. When your run the get-user line by itself from the powershell prompt does it return a mail field for your users?

      Delete
  4. The emails to the users will only go out at the specified intervals. The default is 30 days, 15 days, 7 days, 5 days, 3 days, 2 days, 1 day and the day of password expiration. This means if today is exactly 30 days prior to the user's password expiration then they will get an email. The next day they will not get one. Then when it's 15 days until expiration they will get their second email.

    Does the admin email say that there is no associated email address to email the user? If there is no email address associated with the user, the admin email should include the password expiration reminders that get generated on the intervals. It should only be included in the admin email on the interval though.

    ReplyDelete
    Replies
    1. This was it exactly. I had that idea that night that I was running the script with several test accounts (with expired passwords) instead of live ones. After appending our entire applicable range of password expiry days on the notification trigger date and adding a live account to the AD OU, Voila.

      Thank you again for the great script!

      Delete
  5. Hi,

    Great script. I have been using this script with the domain password policy and it is working fine.

    I am having problems understanding where and how to enter the Get-ADUserResultantPasswordPolicy command when using a fine grain password policy.

    I have entered the following entry " $DefaultDomainPasswordPoliy = Get-ADFineGrainedPasswordPolicy passwordpolicytest"

    thanks for your help

    ReplyDelete
    Replies
    1. Hey Scott, is there any updates on where/how to use a fine grain password policy?

      Delete
    2. Try replacing the GetPasswordExpireDate function. Replace the following:

      function GetPasswordExpireDate($user)
      {
      return [datetime]::FromFileTimeUTC($user.pwdLastSet+$DefaultDomainPasswordPoliy.MaxPasswordAge.Ticks)
      }

      with:

      function GetPasswordExpireDate($user)
      {

      $passwordPolicy = Get-ADUserResultantPasswordPolicy $user.SamAccountName
      #Write-Host $passwordPolicy.MaxPasswordAge
      if ($passwordPolicy -ne $null)
      {
      return [datetime]::FromFileTimeUTC($user.pwdLastSet+$passwordPolicy.MaxPasswordAge.Ticks)
      }
      else
      {
      return [datetime]::FromFileTimeUTC($user.pwdLastSet+$DefaultDomainPasswordPoliy.MaxPasswordAge.Ticks)
      }
      }

      I do not have fine-grained password policies so I did not test this thoroughly. I think it should work though. It should use the file grained policy if one exists for the user and if not it should revert to the default domain policy.

      Do me a favor, run the following power shell snippet and let me know what the output is:

      $users = Get-AdUser -Filter * -Properties userAccountControl, pwdLastSet, userprincipalname, mail, DisplayName, samAccountName, accountExpires, enabled
      foreach( $user in $users )
      {
      $passwordPolicy = Get-ADUserResultantPasswordPolicy $user.SamAccountName
      Write-Host $passwordPolicy.MaxPasswordAge
      }

      This little snippet should get all AD users, try to load the password policy for each user and display the max password age associated to that password policy.

      If this outputs nothing, then replacing the GetPasswordExpireDate function probably won't work as described and we'll need to dig further. If it displays a bunch of integers then replacing the GetPasswordExpireDate function should work for you.

      Let me know.

      Delete
  6. Quick question: how do I know that indeed the user is being notified at those intervals??? What I mean is: I receive the admin email with a list of expired and who needs to change password on next login but it does not include a list of users that have been notified, what code should I add to the script to do that and where should I insert it?.I am not verse at powershell at all
    thanks
    AL

    ReplyDelete
    Replies
    1. Easiest way would be to ask them. I purposely did not include this in the admin emails as I wanted to only get admin emails when something was not quite right. I didn't want to liter the admin emails with superfluous informational. If you want to modify the script, the easiest way would be to copy the AppendAdminEmailNoMail function and call that after the EmailUser function call. Add this:

      function AppendAdminEmailMail($user)
      {
      $pwdExpires = GetPasswordExpireDate $user
      $daysToExpire = GetDaysToExpire $pwdExpires

      return "The password for account """ + $user.samAccountName + """ will expire in " + $daysToExpire + " days at " + $pwdExpires + [System.Environment]::NewLine + [System.Environment]::NewLine
      }

      Then change the following:

      #otherwise email user
      else
      {
      EmailUser $user
      }

      to:

      #otherwise email user
      else
      {
      EmailUser $user
      $adminEmailContent += AppendAdminEmailMail $user
      }

      Delete
    2. Thanks for your work. It is appreciated
      I added your name at the beginning of the scrip on my server for "Credits/Recognition" purposes

      Delete
  7. Is there a way to create exclusions based userAcountControl attribute??...I am getting notifications about an INTER_DOMAIN_TRUST account:
    UserAccountControl=512 = NORMAL account (regular users)

    ReplyDelete
    Replies
    1. You should be able to filter based on a mask of the useraccountcontrol attribute. Can you explain your situation in a little more detail? Do you know which ones should be valid and which ones shouldn't?

      Delete
  8. Any way this can be crafted to function with only a particular domain or OU in AD? We have multiple domains and only want this for one.

    ReplyDelete
    Replies
    1. You should be able to use the Filter or LDAPFilter parameters on the following line:

      $users = Get-AdUser -Filter * -Properties userAccountControl, pwdLastSet, userprincipalname, mail, DisplayName, samAccountName, accountExpires, enabled

      Delete