Wednesday, 8 August 2012

Expiring password notifications with Powershell scripting

Let's say that your users have non-domain joined workstation, but use Outlook or Outlook Web Access to access your e-mail infrastructure. These users would never be notified that their domain password is about to expire. Once it does, their only resort would be to contact administrator and ask him to reset his or her password.

Because of this, I wrote a simple Powershell script that is scheduled to run every day and creates a list of user accounts which passwords will expire in less than 7 days and sends them an e-mail notification that they should change their password. Once a user receives this notification, he can use Outlook Web Access to change his password.

Some notes about the script:

  • The script uses System.Globalization.CultureInfo object to format the date returned for Croatian region (hr-HR). If you come from US, you can change hr-HR in the script to en-US
  • The $msg.Body property in the script contains HTML formatted text that user receives as an e-mail message body. You can modify this HTML code, for example, to include your company branding
  • You can modify the $OU property so that script only scopes your external users, users from a specific department, etc.
  • The script does not send e-mail to users whose passwords are already expired or who have "Password never expires" property set
  • You can modify how many days are left until password expiry before you start notifying users. Currently, script notifies users whose passwords expire in less than 7 days.
    You can modify the following line: $DaysToExpire.Days -lt 7
  • The policy works with default domain password policy and with Password Settings Object, a feature of Windows Server 2008 Active Directory
  • You must run the script on a domain controller or on a server with Remote Server Administration Tools installed
  • You must change the variable $smtpServer to your internal mail server (usually Exchange Hub Transport server) that can send e-mails to your users

Here is the script:


PowerGUI Script Editor
Import-Module ActiveDirectory

#System globalization
$ci = New-Object System.Globalization.CultureInfo("hr-HR")

#SMTP server name
$smtpServer = "smtp.domain.local"

#Creating SMTP server object
$smtp = new-object Net.Mail.SmtpClient($smtpServer)

#E-mail structure
Function EmailStructure($to,$expiryDate,$upn)
{

 #Creating a Mail object
 $msg = new-object Net.Mail.MailMessage

 $msg.IsBodyHtml = $true
 $msg.From = "administrator@domain.com"
 $msg.To.Add($to)
 $msg.Subject = "Password expiration notice"
 $msg.Body = "<html><body><font face='Arial'>This is an automatically generated message from Service Provider Exchange service.<br><br><b>Please note that the password for your account $upn will expire on $expiryDate.</b><br><br>Please change your password immediately or at least before this date as you will be unable to access the service without contacting your administrator.</font></body></html>"
 
 return $msg
}


#Set the target OU that will be searched for user accounts
$OU = "OU=Users,DC=domain,DC=local"

$ADAccounts = Get-ADUser -LDAPFilter "(objectClass=user)" -searchbase $OU -properties PasswordExpired, PasswordNeverExpires, PasswordLastSet, Mail, Enabled | Where-object {$_.Enabled -eq $true -and $_.PasswordNeverExpires -eq $false}

Foreach ($ADAccount in $ADAccounts)
{
 $accountFGPP = Get-ADUserResultantPasswordPolicy $ADAccount

                if ($accountFGPP -ne $null) {
                   $maxPasswordAgeTimeSpan = $accountFGPP.MaxPasswordAge
                } else {
                   $maxPasswordAgeTimeSpan = (Get-ADDefaultDomainPasswordPolicy).MaxPasswordAge
                }

  #Fill in the user variables
  $samAccountName = $ADAccount.samAccountName
  $userEmailAddress = $ADAccount.Mail
  $userPrincipalName = $ADAccount.UserPrincipalName

  if ($ADAccount.PasswordExpired) {
   Write-host "The password for account $samAccountName has expired!"
  } else {
   $ExpiryDate = $ADAccount.PasswordLastSet + $maxPasswordAgeTimeSpan
   Write-host "The password for account $samAccountName expires on: $ExpiryDate"

   $TodaysDate = Get-Date
   $DaysToExpire = $ExpiryDate - $TodaysDate
   #Write-Host $DaysToExpire.Days
   if ($DaysToExpire.Days -lt 7) {
    $expiryDate = $expiryDate.ToString("d",$ci)
    
    #Generate e-mail structure and send message
    $msg = EmailStructure $userEmailAddress $expiryDate $userPrincipalName
    $smtp.Send($msg)

    Write-Host "The expiration notification e-mail was sent to $userEmailAddress"
   }

  }
}

14 comments:

  1. Hi,

    In the notification email there is an issue, the recepient is present many times in the "$to" , do you know why ?

    thank you!

    To patch, here is :

    Function EmailStructure($smtpServer,$to,$expiryDate,$upn)
    {

    #Creating SMTP server object
    $smtp = new-object system.net.mail.smtpClient($smtpServer)
    #Creating a Mail object
    $msg = new-object System.Net.Mail.MailMessage

    $msg.IsBodyHtml = $true
    $msg.From = "you@mail.com"
    $msg.To.Add($to)
    Write-Host $to
    $msg.Subject = "your subject here"
    $msg.Body = "your body here"
    $smtp.send($msg)
    }

    ReplyDelete
    Replies
    1. Thank you very much for pointing out an issue. There was a bug in the code where I used the same $msg object over and over and was just appending the e-mail addresses in the to list.
      It has now been corrected.

      Enjoy and thanks once again!

      Delete
  2. You're welcome :)

    I also modify :
    $DaysToExpire.Days -eq 7

    now I can run the script every day and the user receive just one email :)

    ReplyDelete
  3. This is a really good article.

    How do you schedule this Powershell script to run once a day? Do you use the task scheduler in Administrative Tools?

    Thanks

    RK

    ReplyDelete
    Replies
    1. Thanks Rob! Yes, I use task scheduler in Administrative Tools to schedule it once a day.

      Delete
  4. Mr Xhark,

    Where did you put the "$DaysToExpire.Days -eq 7" so users only receive one email?

    ReplyDelete
  5. Hi Dinko,

    Is the script at the top of the page the latest? I'm asking because its not working for me as expected. I'm receiving emails from the script, but it doesn't specify the date of expiration. Below are the errors I'm receiving:
    -- any help would be greatly appreciated:

    Get-ADDefaultDomainPasswordPolicy : Cannot find an object with identity: 'Micro
    soft.ActiveDirectory.Management.ADDefaultDomainPasswordPolicy' under: 'DC=child,DC
    =domain,DC=local'.
    At C:\free_script.ps1:41 char:80
    + $maxPasswordAgeTimeSpan = (Get-ADDefaultDomainPasswordPoli
    cy <<<< ).MaxPasswordAge
    + CategoryInfo : ObjectNotFound: (Microsoft.Activ...nPasswordPoli
    cy:ADDefaultDomainPasswordPolicy) [Get-ADDefaultDomainPasswordPolicy], ADI
    dentityNotFoundException
    + FullyQualifiedErrorId : Cannot find an object with identity: 'Microsoft.
    ActiveDirectory.Management.ADDefaultDomainPasswordPolicy' under: 'DC=child,DC
    =domain,DC=local'.,Microsoft.ActiveDirectory.Management.Commands.GetADDe
    faultDomainPasswordPolicy

    Cannot convert argument "1", with value: "", for "op_Addition" to type "System.
    TimeSpan": "Cannot convert null to type "System.TimeSpan"."
    At C:\script.ps1:52 char:46
    + $ExpiryDate = $ADAccount.PasswordLastSet + <<<< $maxPasswordAgeTimeSpan
    + CategoryInfo : NotSpecified: (:) [], MethodException
    + FullyQualifiedErrorId : MethodArgumentConversionInvalidCastArgument

    The password for account jdoe expires on:
    The operation '[$null] - [System.DateTime]' is not defined.
    At C:\script.ps1:56 char:33
    + $DaysToExpire = $ExpiryDate - <<<< $TodaysDate
    + CategoryInfo : InvalidOperation: (:) [], RuntimeException
    + FullyQualifiedErrorId : NotAdefinedOperationForTypeType

    You cannot call a method on a null-valued expression.
    At C:\script.ps1:59 char:39
    + $expiryDate = $expiryDate.ToString <<<< ($ci)
    + CategoryInfo : InvalidOperation: (ToString:String) [], RuntimeE
    xception
    + FullyQualifiedErrorId : InvokeMethodOnNull

    ReplyDelete
    Replies
    1. Adrian,

      This is the latest script. It seems that the problem could be something with your environment.
      Can you just run the following two lines in your Powershell window on domain controller and see where it takes you.

      Import-Module ActiveDirectory
      Get-ADDefaultDomainPasswordPolicy

      If everything is okay, you should get the domain password settings in the output.

      Regards,
      Dinko

      Delete
  6. This comment has been removed by the author.

    ReplyDelete
  7. PS C:\Windows\system32> Import-Module ActiveDirectory
    PS C:\Windows\system32> Get-ADDefaultDomainPasswordPolicy
    Get-ADDefaultDomainPasswordPolicy : Cannot find an object with identity: 'Microsoft.ActiveDirectory.Management.ADDefaul
    tDomainPasswordPolicy' under: 'DC=child,DC=domain,DC=local'.
    At line:1 char:34
    + Get-ADDefaultDomainPasswordPolicy <<<<
    + CategoryInfo : ObjectNotFound: (Microsoft.Activ...nPasswordPolicy:ADDefaultDomainPasswordPolicy) [Get-A
    DDefaultDomainPasswordPolicy], ADIdentityNotFoundException
    + FullyQualifiedErrorId : Cannot find an object with identity: 'Microsoft.ActiveDirectory.Management.ADDefaultDoma
    inPasswordPolicy' under: 'DC=child,DC=domain,DC=local'.,Microsoft.ActiveDirectory.Management.Commands.GetADDefaultD
    omainPasswordPolicy

    ReplyDelete
  8. Hi Dinko,

    Please see the post just before this one for the output after running the two lines as requested. Also, thanks again for your assistance here.

    Adrian

    ReplyDelete
  9. Really nice work Dinko. I am trying to work the script to against local accounts instead of AD. Any tips on the matter?

    ReplyDelete
  10. I too am getting this:
    PS C:\Windows\system32> Get-ADDefaultDomainPasswordPolicy
    Get-ADDefaultDomainPasswordPolicy : Cannot find an object with identity: 'Microsoft.ActiveDirectory.Management.ADDefaul
    tDomainPasswordPolicy' under: 'DC=child,DC=domain,DC=local'.
    At line:1 char:34

    I can run your first line fine but when I run the second get the same error

    ReplyDelete
  11. You need Domain Admin permissions to run the command or delegate read permissions on the msDS-PasswordSettingsContainer Object I think. Then it should work too. By default Domain Users have no read permissions on the PSOs.

    ReplyDelete