Zachary Loeber

The personal website of Zachary Loeber.

Powershell: New-ADPasswordReminder

A single, self-extracting, self-scheduling, AD password change notice PowerShell script.

One of the great things about sharing PowerShell code is that it can be expanded upon by the community. Here is a case where I’ve taken someone else’s great work and made it (hopefully) a little bit better.


This script notifies users that their password is about to expire. I’ve taken the work done by others and added some features to make it easier to schedule and manage.

The overall changes to the original script were meant to facilitate multiple runs of the script to send out notifications in a less spammy way. For example:

  1. 7 Days until the password will expire -> Send a warning (yellow) email notice
  2. 3 Days until the password will expire -> Send an alert (red) email notice
  3. 1 Day until the password will expire -> Send a final alert (red) notice

I’ve also made numerous readability and other feature additions. This includes (but is not limited to);

  1. Ability to distribute the script in 1 file and self-extract the gif files used in the notices.
  2. Total rewrite of the notice generation to be a bit more centralized (and therefore easier to locate and customize if needed)
  3. Elimination of global variables
  4. Conversion of all local variables into parameters
  5. Self-referencing scheduled task installation routine (schedules the task with the same parameters the script itself was passed)
  6. A ‘LooseMatching’ mode to facilitate exact matching.

If you want the dummy way of running this script simply copy to your server and run the following command:

Show-Command .\New-ADPasswordReminder.ps1

You then get a nice little GUI like this:

The examples below should be sufficient to get you started with this script.

.\New-ADPasswordReminder.ps1 -ExtractGifs

Extract the embedded gifs used in the notifications generated by this script. These should be then moved over to a publicly available web server.

.\New-ADPasswordReminder.ps1 -Demo -DaysToWarn 5

Searches Active Directory for users who have passwords expiring in 5 days, and lists those users on the screen, along with days till expiration and policy setting

 .\New-ADPasswordReminder.ps1 -DaysToWarn 7 -Demo -LooseMatching

Query AD for any accounts with passwords that will expire in 7 days or less and print them to the screen.

 .\New-ADPasswordReminder.ps1 -DaysToWarn 7 -Demo

Query AD for any accounts with passwords that will expire in EXACTLY 7 days and print them to the screen.

.\New-ADPasswordReminder.ps1  -Alert -PreviewUser 'jdoe' -Company 'Contoso' -PasswordChangeURL '' -EmailServer '' -EmailFrom 'IT ServiceDesk <[email protected]>' -HelpDeskPhone '(555) 555-5555' -ImagePath '' -Install

Create a new scheduled task test for the jdoe user. This will automatically assume that jdoe's password changes in a day and send a red (alert) email notification.

$MyCred = Get-Credential
.\New-ADPasswordReminder.ps1 -Company 'Contoso' -PasswordChangeURL '' -EmailServer '' -EmailFrom 'IT ServiceDesk <[email protected]>' -HelpDeskPhone '(555) 555-5555' -ImagePath '' -Install -DaysToWarn 7 -Credential $MyCred

.\New-ADPasswordReminder.ps1 -Company 'Contoso' -PasswordChangeURL '' -EmailServer '' -EmailFrom 'IT ServiceDesk <[email protected]>' -HelpDeskPhone '(555) 555-5555' -ImagePath '' -Install -DaysToWarn 3 -Alert -Credential $MyCred

.\New-ADPasswordReminder.ps1 -Company 'Contoso' -PasswordChangeURL '' -EmailServer '' -EmailFrom 'IT ServiceDesk <[email protected]>' -HelpDeskPhone '(555) 555-5555' -ImagePath '' -Install -DaysToWarn 1 -Alert -Credential $MyCred

Create a new scheduled task that will send a warning notice on a daily basis to users that have passwords that will expire in 7 days then again in 3 days. Also, create another similar task that will send an alert to users that have one more day to change their passwords.


Running as a Managed Service Account

In newer versions of Windows AD you can create managed service accounts which reduce yet another account password you have to keep note of in your environment. I’ve tested this script using an MSA and it seems to work just fine. Here are the steps you’d need to follow to update the scheduled tasks created by this script to use an MSAs instead.

Firstly you will need to setup your MSA. Replace anything in angle brackets as needed (Computer = the server you will be scheduling the tasks on):

New-ADServiceAccount -SamAccountName "ADPWNotice" -Name "ADPWNotice" -Description "Account used for running the AD PW notification task on <Computer>" -DNSHostName <Domain Controller>
Set-ADServiceAccount -Identity ADPWNotice -PrincipalsAllowedToRetrieveManagedPassword <Computer>$ -Enabled $true
Add-ADComputerServiceAccount -Identity "<Domain Controller>" -ServiceAccount "ADPWNotice"
Add-ADGroupMember "Domain Users" "CN=ADPWNotice,CN=Managed Service Accounts,DC=<contoso>,DC=<com>"
Add-ADGroupMember "Backup Operators" "CN=ADPWNotice,CN=Managed Service Accounts,DC=<contoso>,DC=<com>"

Next create your scheduled tasks like you normally would. When prompted for a user id/password put in your administrative account. This will only be a temporary assignment until you update with the MSA.

This example will schedule a password change notification 7 days before it needs to be changed.

.\New-ADPasswordReminder.ps1 -Company 'Contoso' -PasswordChangeURL '' -EmailServer '' -EmailFrom 'IT ServiceDesk <[email protected]>' -HelpDeskPhone '(555) 555-5555' -ImagePath '' -Install -DaysToWarn 7

You will need to update the computer where the scheduled tasks will run to allow the MSA some local rights. Here is a handy script to do just that (notice the MSA includes the $ at the end).

Function Add-UserToLocalSecurityRight {
    When run administratively this will add a user to the local system's login local rights security policy.
    When run administratively this will add a user to the local system's login local rights security policy.
    User ID to add to the local system's login local rights security policy.
    .PARAMETER LocalRight
    Local right to grant. Either 'LogonAsBatch' or 'LogonLocal'
        1.0.0 - Initial release
        1.0.1 - Updated to include logon local right and parameter to select the right to assign.
        Zachary Loeber
        Code mildy modified from

    Add-UserToLoginAsBatch 'test.user' -LogonRight 'LogonAsBatch'

    Adds the local user test.user to the login as batch job rights on the local machine.
        [string]$LocalRight = 'LogonAsBatch'

    $CSharpCode = @'
    using System;
    // using System.Globalization;
    using System.Text;
    using System.Runtime.InteropServices;
    public class LsaWrapper
    // Import the LSA functions

    [DllImport("advapi32.dll", PreserveSig = true)]
    private static extern UInt32 LsaOpenPolicy(
        ref LSA_UNICODE_STRING SystemName,
        ref LSA_OBJECT_ATTRIBUTES ObjectAttributes,
        Int32 DesiredAccess,
        out IntPtr PolicyHandle

    [DllImport("advapi32.dll", SetLastError = true, PreserveSig = true)]
    private static extern long LsaAddAccountRights(
        IntPtr PolicyHandle,
        IntPtr AccountSid,
        LSA_UNICODE_STRING[] UserRights,
        long CountOfRights);

    public static extern void FreeSid(IntPtr pSid);

    [DllImport("advapi32.dll", CharSet = CharSet.Auto, SetLastError = true, PreserveSig = true)]
    private static extern bool LookupAccountName(
        string lpSystemName, string lpAccountName,
        IntPtr psid,
        ref int cbsid,
        StringBuilder domainName, ref int cbdomainLength, ref int use);

    private static extern bool IsValidSid(IntPtr pSid);

    private static extern long LsaClose(IntPtr ObjectHandle);

    private static extern int GetLastError();

    private static extern long LsaNtStatusToWinError(long status);

    // define the structures

    private enum LSA_AccessPolicy : long
        POLICY_TRUST_ADMIN = 0x00000008L,
        POLICY_CREATE_ACCOUNT = 0x00000010L,
        POLICY_CREATE_SECRET = 0x00000020L,
        POLICY_CREATE_PRIVILEGE = 0x00000040L,
        POLICY_AUDIT_LOG_ADMIN = 0x00000200L,
        POLICY_SERVER_ADMIN = 0x00000400L,
        POLICY_LOOKUP_NAMES = 0x00000800L,
        POLICY_NOTIFICATION = 0x00001000L

    private struct LSA_OBJECT_ATTRIBUTES
        public int Length;
        public IntPtr RootDirectory;
        public readonly LSA_UNICODE_STRING ObjectName;
        public UInt32 Attributes;
        public IntPtr SecurityDescriptor;
        public IntPtr SecurityQualityOfService;

    private struct LSA_UNICODE_STRING
        public UInt16 Length;
        public UInt16 MaximumLength;
        public IntPtr Buffer;
    //Adds a privilege to an account

    /// Name of an account - "domain\account" or only "account"
    /// Name of the privilege
    /// The windows error code returned by LsaAddAccountRights
    public long SetRight(String accountName, String privilegeName)
        long winErrorCode = 0; //contains the last error

        //pointer an size for the SID
        IntPtr sid = IntPtr.Zero;
        int sidSize = 0;
        //StringBuilder and size for the domain name
        var domainName = new StringBuilder();
        int nameSize = 0;
        //account-type variable for lookup
        int accountType = 0;

        //get required buffer size
        LookupAccountName(String.Empty, accountName, sid, ref sidSize, domainName, ref nameSize, ref accountType);

        //allocate buffers
        domainName = new StringBuilder(nameSize);
        sid = Marshal.AllocHGlobal(sidSize);

        //lookup the SID for the account
        bool result = LookupAccountName(String.Empty, accountName, sid, ref sidSize, domainName, ref nameSize,
                                        ref accountType);

        //say what you're doing
        Console.WriteLine("LookupAccountName result = " + result);
        Console.WriteLine("IsValidSid: " + IsValidSid(sid));
        Console.WriteLine("LookupAccountName domainName: " + domainName);

        if (!result)
            winErrorCode = GetLastError();
            Console.WriteLine("LookupAccountName failed: " + winErrorCode);
            //initialize an empty unicode-string
            var systemName = new LSA_UNICODE_STRING();
            //combine all policies
            var access = (int) (
                                    LSA_AccessPolicy.POLICY_AUDIT_LOG_ADMIN |
                                    LSA_AccessPolicy.POLICY_CREATE_ACCOUNT |
                                    LSA_AccessPolicy.POLICY_CREATE_PRIVILEGE |
                                    LSA_AccessPolicy.POLICY_CREATE_SECRET |
                                    LSA_AccessPolicy.POLICY_GET_PRIVATE_INFORMATION |
                                    LSA_AccessPolicy.POLICY_LOOKUP_NAMES |
                                    LSA_AccessPolicy.POLICY_NOTIFICATION |
                                    LSA_AccessPolicy.POLICY_SERVER_ADMIN |
                                    LSA_AccessPolicy.POLICY_SET_AUDIT_REQUIREMENTS |
                                    LSA_AccessPolicy.POLICY_SET_DEFAULT_QUOTA_LIMITS |
                                    LSA_AccessPolicy.POLICY_TRUST_ADMIN |
                                    LSA_AccessPolicy.POLICY_VIEW_AUDIT_INFORMATION |
            //initialize a pointer for the policy handle
            IntPtr policyHandle = IntPtr.Zero;

            //these attributes are not used, but LsaOpenPolicy wants them to exists
            var ObjectAttributes = new LSA_OBJECT_ATTRIBUTES();
            ObjectAttributes.Length = 0;
            ObjectAttributes.RootDirectory = IntPtr.Zero;
            ObjectAttributes.Attributes = 0;
            ObjectAttributes.SecurityDescriptor = IntPtr.Zero;
            ObjectAttributes.SecurityQualityOfService = IntPtr.Zero;

            //get a policy handle
            uint resultPolicy = LsaOpenPolicy(ref systemName, ref ObjectAttributes, access, out policyHandle);
            winErrorCode = LsaNtStatusToWinError(resultPolicy);

            if (winErrorCode != 0)
                Console.WriteLine("OpenPolicy failed: " + winErrorCode);
                //Now that we have the SID an the policy,
                //we can add rights to the account.

                //initialize an unicode-string for the privilege name
                var userRights = new LSA_UNICODE_STRING[1];
                userRights[0] = new LSA_UNICODE_STRING();
                userRights[0].Buffer = Marshal.StringToHGlobalUni(privilegeName);
                userRights[0].Length = (UInt16) (privilegeName.Length*UnicodeEncoding.CharSize);
                userRights[0].MaximumLength = (UInt16) ((privilegeName.Length + 1)*UnicodeEncoding.CharSize);

                //add the right to the account
                long res = LsaAddAccountRights(policyHandle, sid, userRights, 1);
                winErrorCode = LsaNtStatusToWinError(res);
                if (winErrorCode != 0)
                    Console.WriteLine("LsaAddAccountRights failed: " + winErrorCode);


        return winErrorCode;

    public class AddUserLocalRight
        public static void GrantUserLogonAsBatchJob(string userName)
                LsaWrapper lsaUtility = new LsaWrapper();

                lsaUtility.SetRight(userName, "SeBatchLogonRight");

                Console.WriteLine("Logon as batch job right is granted successfully to " + userName);
            catch (Exception ex)
        public static void GrantUserLogonLocal(string userName)
                LsaWrapper lsaUtility = new LsaWrapper();

                lsaUtility.SetRight(userName, "SeInteractiveLogonRight");

                Console.WriteLine("Logon local right is granted successfully to " + userName);
            catch (Exception ex)
    try {
        Add-Type -ErrorAction Stop -Language:CSharpVersion3 -TypeDefinition $CSharpCode
    catch {
        Write-Error $_.Exception.Message
    if ($LocalRight -eq 'LogonAsBatch') {
    if ($LocalRight -eq 'LogonLocal') {

Add-UserToLocalSecurityRight -UserID "<Domain>\ADPWNotice$" -LocalRight 'LogonAsBatch'
Add-UserToLoginAsBatch "<Domain>\ADPWNotice$" -LocalRight 'LogonLocal'

Now that you have your starting scheduled task you will need to update it to use the MSA. In this example I create a new scheduled task like the existing one but using the MSA instead.

$oldtask = Get-ScheduledTask "AD Password Expiration Notification (7 Day Warning)"
$trigger = New-ScheduledTaskTrigger -At 12:00 -Daily 
$principal = New-ScheduledTaskPrincipal -UserID "<Domain>\ADPWNotice$" -LogonType Password
Register-ScheduledTask "AD Password Expiration Notification (7 Day Warning) - MSA" –Action $oldtask.Actions[0] –Trigger $trigger –Principal $principal

I think you can just update the task with the MSA and an empty password as well:

schtasks /Change /TN "AD Password Expiration Notification (7 Day Alert)" /RU "ADPWNotice$" /RP ""

And that’s all there is to it really. The scheduled task will run if all the local rights are set and you prayed to the correct IT gods and the phase of the moon is just right.


Other Information

Github Project Site:

Original Script Site: