Monday, October 18, 2021

Microsoft 365 Password Expiration Notification Email Solution for On-Premises AD Accounts

"Hello Again World!"

Hi there! Mike Kullish, here. I'm a Microsoft Customer Engineer (CE) based just off the Gulf Coast of  Florida with a focus on AD, Hyper-V and DFS, but I try to help customers with anything on the Windows Desktop and/or Server platforms. (Also, this whole Azure thing has become a big deal, so I dabble with that as well…) I have been with Microsoft for over nine years and this is a follow-up to my first blog post written about 6 years ago which can be found here: How to Setup a Password Expiration Notification Email Solution - Microsoft Tech Community. The changes below help to eliminate the use of SMTP servers and allow you to use Microsoft Graph to send emails. This is a more modern take on the original article. I must acknowledge two of my CE colleagues that helped this update come together. First David Morillo, who assisted in wordsmithing the article, and secondly the mastermind behind the script that makes it all work, Daniel Carroll.

Have you ever had a need to configure notifications for user's password expirations but found that existing solutions didn't quite fit the bill? We all know you can use built-in solutions with Windows and Active Directory/Group Policy but this requires users to interactively log-on to a domain joined computer. What about those BYOD or mobile users or users of web apps/email? Typically, these users will have to call the helpdesk because they had no idea their domain passwords were going to expire. Statistics show that some of the most common calls to the helpdesk are password-related and implementing a process like the one covered here could really make a dent in your helpdesk call volume and costs.

The first article mentioned above enabled you to use an existing SMTP server to configure and send emails to users that had passwords which were about to expire. This solution was based on the Send-MailMessage cmdlet which is now considered obsolete. Below, we will describe the process to setup a script that uses Microsoft Graph to send email using your M365 subscription.

 

MichaelKullish_0-1632940746924.png

 

I thought it would make a helpful blog post to cover some of the details and considerations when implementing a solution like this. As mentioned, Daniel Carroll deserves credit for the script that follows.

 

DISCLAIMER:

  • The sample scripts are not supported under any Microsoft standard support program or service. The sample scripts are provided AS IS without warranty of any kind. Microsoft further disclaims all implied warranties including, without limitation, any implied warranties of merchantability or of fitness for a particular purpose. The entire risk arising out of the use or performance of the sample scripts and documentation remains with you. In no event shall Microsoft, its authors, or anyone else involved in the creation, production, or delivery of the scripts be liable for any damages whatsoever (including, without limitation, damages for loss of business profits, business interruption, loss of business information, or other pecuniary loss) arising out of the use of or inability to use the sample scripts or documentation, even if Microsoft has been advised of the possibility of such damages.
  • CEs don't normally provide code beyond sample or "proof of concept" code
  • The code we discuss here is an additional layer beyond code from a CE; it is code from a passion project of Dan’s that was trimmed to fit the need of this article.
  • As with ANY code, you should always test/validate its behavior in an isolated lab
  •  Did I mention you should test/validate the code? I have also validated that it works on Windows Server 2019 and Windows Server 2022.
  • If/when you're ready to deploy it to production, you should employ a solid change control process and a controlled release. This code can possibly generate emails to thousands of users .

You can download the script from the following link .  I will also post a full copy of the code within the article, but updates will be made more frequently on GitHub. ( Script to Automate Email Reminders when Users Passwords due to Expire using O365 Shared Mailbox · GitHub )

Click on the blue box and save the file to a workstation or member server. Obviously, a DC would work but likely isn't the best choice. The workstation or member server needs the RSAT tools for Active Directory installed. If you already have an "admin server" system where you have existing scripts, tools, Scheduled Tasks, etc., that would be a logical place for this. I would also suggest creating a folder such as C:\temp on the tools machine in order to ensure you can follow along easily with the instructions below.

 

 

#################################################################################################################
#
# Original Robert Pearman v1.4 Passowrd Change Notification
#    - Adapted to support O365 SendAS Shared Mailbox
# Script to Automate Email Reminders when Users Passwords due to Expire using O365 Shared Mailbox.
#
# Requires: 
#        Windows PowerShell Module for Active Directory
#        Azure AD Application registration with MS Graph Application Mail.Send permission
#
#
##################################################################################################################
# Please Configure the following variables....
$expireindays = 21
$logging = "Enabled" # Set to Disabled to Disable Logging
$logFile = "" # ie. c:\mylog.csv
$testing = "Enabled" # Set to Disabled to Email Users
$testRecipient = ''
$clientId = '' # App registration ID used to send on behalf of shared mailbox
$clientSecret = (Import-Clixml -Path $PSScriptRoot\SendEmailSecret.ps1.credential).GetNetworkCredential().Password #Client Secret credential file
$tenantName = '' #TenantName
$SendEmailAccount = '' #SharedMailbox name
$resource = 'https://graph.microsoft.com' #Graph Endpoint https://graph.microsoft.com or https://graph.microsoft.us or https://dod-graph.microsoft.us
#
###################################################################################################################

$ReqTokenBody = @{
    Grant_Type    = "client_credentials"
    Scope         = "$($resource)/.default"
    client_Id     = $clientID
    Client_Secret = $clientSecret
} 
Try {
    $params = @{
        Uri         = "https://login.microsoftonline.com/$TenantName/oauth2/v2.0/token"
        Method      = "POST"
        ErrorAction = "Stop"
    }
    $TokenResponse = Invoke-RestMethod @params -Body $ReqTokenBody

    if ($TokenResponse) {
        # Check Logging Settings
        if (($logging) -eq "Enabled")
        {
            # Test Log File Path
            $logfilePath = (Test-Path $logFile)
            if (($logFilePath) -ne "True")
            {
                # Create CSV File and Headers
                New-Item $logfile -ItemType File
                Add-Content $logfile "Date,Name,EmailAddress,DaystoExpire,ExpiresOn,Notified"
            }
        } # End Logging Check

        # System Settings
        $textEncoding = [System.Text.Encoding]::UTF8
        $date = Get-Date -format ddMMyyyy
        # End System Settings

        # Get Users From AD who are Enabled, Passwords Expire and are Not Currently Expired
        Import-Module ActiveDirectory
        $users = get-aduser -filter * -properties Name, PasswordNeverExpires, PasswordExpired, PasswordLastSet, EmailAddress |where {$_.Enabled -eq "True"} | where { $_.PasswordNeverExpires -eq $false } | where { $_.passwordexpired -eq $false }
        $DefaultmaxPasswordAge = (Get-ADDefaultDomainPasswordPolicy).MaxPasswordAge

        # Process Each User for Password Expiry
        foreach ($user in $users)
        {
            $Name = $user.Name
            $emailaddress = $user.emailaddress
            $passwordSetDate = $user.PasswordLastSet
            $PasswordPol = (Get-AduserResultantPasswordPolicy $user)
            $sent = "" # Reset Sent Flag
            # Check for Fine Grained Password
            if (($PasswordPol) -ne $null)
            {
                $maxPasswordAge = ($PasswordPol).MaxPasswordAge
            }
            else
            {
                # No FGP set to Domain Default
                $maxPasswordAge = $DefaultmaxPasswordAge
            }


            $expireson = $passwordsetdate + $maxPasswordAge
            $today = (get-date)
            $daystoexpire = (New-TimeSpan -Start $today -End $Expireson).Days

            # Set Greeting based on Number of Days to Expiry.

            # Check Number of Days to Expiry
            $messageDays = $daystoexpire

            if (($messageDays) -gt "1")
            {
                $messageDays = "in " + "$daystoexpire" + " days."
            }
            else
            {
                $messageDays = "today."
            }
                # If Testing Is Enabled - Email Administrator
            if (($testing) -eq "Enabled")
            {
                $emailaddress = $testRecipient
            } # End Testing

            # If a user has no email address listed
            if (($emailaddress) -eq $null)
            {
                $emailaddress = $testRecipient
            }# End No Valid Email

            # Email Subject Set Here
            $subject="Your password will expire $messageDays"

            # Email Body Set Here, Note You can use HTML.
            $body = @"
            {
              "Message": {
                "Subject": "$($subject)",
                "importance":"High",
                "Body": {
                  "ContentType": "HTML",
                  "Content": "<p>Dear $($name),</p>
            <p> Your Password will expire $($messageDays)<br>
            To change your password on a PC press CTRL ALT Delete and choose Change Password <br>
            <p>Thanks, <br>
            </P>"
                },
                "ToRecipients": [
                  {
                    "EmailAddress": {
                      "Address": "$($emailaddress)"
                    }
                  }
                ]
              },
              "SaveToSentItems": "false",
              "isDraft": "false"
            }
"@

            # Send Email Message
            if (($daystoexpire -ge "0") -and ($daystoexpire -lt $expireindays))
            {
                $sent = "Yes"
                # If Logging is Enabled Log Details
                if (($logging) -eq "Enabled")
                {
                    Add-Content $logfile "$date,$Name,$emailaddress,$daystoExpire,$expireson,$sent"
                }
                # Send Email Message
                $apiUrl = "$resource/v1.0/users/$SendEmailAccount/sendMail"
                Invoke-RestMethod -Headers @{Authorization = "Bearer $($Tokenresponse.access_token)"} -Uri $apiUrl -Body $Body -Method Post -ContentType 'application/json'

            } # End Send Message
            else # Log Non Expiring Password
            {
                $sent = "No"
                # If Logging is Enabled Log Details
                if (($logging) -eq "Enabled")
                {
                    Add-Content $logfile "$date,$Name,$emailaddress,$daystoExpire,$expireson,$sent"
                }
            }
        }

        } # End User Processing

} catch {
    [System.ApplicationException]::new("Failed to aquire token")
}

 

 

Once you have downloaded or created your own copy of the script:

  • Create a shared mailbox in M365 to be used if you haven’t already done so.
    • In the example for this article, we will use smtp1.contoso.com
  • Log into Azure AD and register a new App by selecting “App registrations”
    • Select New registration and fill in the fields as appropriate

2A.png

    • Click Register
  • Copy Application ID from the Overview section of the newly created SendMail app:

3A.png

 

  • Place Application (client) ID GUID on line 19 of script you downloaded
  • From the SendMail application blade in Azure AD Click on “Certificates & secrets”
    • Click on New client secret
    • Choose options like below or as appropriate for your organization

5B.png

  • Click Add when finished
    • When you create the secret, this is the only time you can get it (You should copy this to the clipboard and to a file if necessary): 3AA7Q~YSlo1235Pxvjq6U7vE6uanqXYZTOqC5
      • Don’t share this information. You will have to recreate the secret if you ever want it again and don’t save a copy.
  • Open an elevated PowerShell prompt
  • Run the following from an elevated PowerShell (This will add the client secret from your app to the script using DPAPI and pins the credentials to the tools machine based on the user running the script…am I stressing this point yet?):
    • $cred = ’3AA7Q~YSlo1235Pxvjq6U7vE6uanqXYZTOqC5'
      [System.Management.Automation.PSCredential]::new( "SendEmailCred", (ConvertTo-SecureString -String $cred -AsPlainText -Force) ) | Export-Clixml -Path C:\temp\test.ps1.credential
  • Fill in $tenantName and $testRecipient in script
  • Set Permissions on the app:
    • Goto the app you just created in Azure AD
    • Choose API Permissions from the blade on the left
    • Choose “Application permissions” and the Mail.Send permission while in the blade as depicted below:

7A.png

    • Choose Add permissions when ready
    • Then click on Grant admin consent for your domain when prompted

7B.png

    • You should then see permissions similar to the following:

7C.png

  • Place the .PS1 file in a directory on your admin server. (For this example, I will use C:\temp)
  • Edit the following portions of the script as applicable using Notepad or PowerShell ISE if you haven’t already done so:
    • $expireindays = 21 
      • This is the number of days prior to password expiration that you want to notify users. The actual number of days remaining before expiration will be displayed in the email notification.
    • $logging = "Enabled" # Set to Disabled to Disable Logging
      • Logging is recommended to ensure that you can trace any errors that might occur
    • $logFile = "C:\temp\pwdexplog.csv"
      • This field should be changed to a desired location on the local system or network share as desired.
    • $testing = "Enabled"
      • Set to Disabled to email users (configuring this to Enabled, runs a check against all accounts and sends emails ONLY the account specified in the $testRecipient field below.)
        • Configuring this to disabled actually sends emails to the users that will have their passwords expire in the configured amount of time.
        • Understand this - you risk sending out a mass-email to 10s, 100s or 10,000s of users.
        • This is automation – with great power, comes great responsibility
    • $testRecipient = "user@domain.com"
      • This will provide a test recipient email address to ensure the script is working properly
    • $clientId = ""
      • This will be the app registration ID used to send on behalf of as shared mailbox in use (example: 71267c5f-b88d-aaaa-a3e4-370d101234ac)
    • $tenantName = ""
      • This is the name of your tenant (Example: Contoso.onmicrosoft.com)
    • $SendEmailAccount = "someone@company.com"
    • $clientSecret  = (Import-Clixml -Path C:\Scripts\sendemailsecret.ps1.credential).GetNetworkCredential().password #Client Secret from AAD
      • This secret can be found by running the following on the tools machine. Note: It is important that the account running the following scripts is also the account used to manually run the PowerShell script manually or to run any scheduled tasks which may use the script from GitHub. (Reference Article: Export-Clixml (Microsoft.PowerShell.Utility) - PowerShell | Microsoft Docs)

 

 

$cred = '3BW7Q~YSlo2oS5Pxvjq6U7vE6uanqQnGTOqC5' 
[System.Management.Automation.PSCredential]::new( "SendEmailCred", (ConvertTo-SecureString -String $cred -AsPlainText -Force) ) | Export-Clixml -Path C:\temp\SendEmailSecret.ps1.credential

#The credential specified in $cred above should be gathered when creating the App Registration

 

 

  • Save the script once you are done editing it.
  • Now you can test the script by running it in a lab.
    • You may need to modify the execution policy for PowerShell scripts on your admin server machine.
  • You should get an email that looks something like this:
  • From: someone@company.com [mailto:someone@company.com]
    Sent: Thursday, September 28, 2021 12:52 PM
    To: Someone@company.com
    Dear chptest,

    To change your password on a PC press CTRL-ALT-Delete and choose Change Password

    Thanks,

 

  • It is important to ensure that you change the section of the script under $body .  The message should be modified to ensure that users don't accidentally delete the email because they suspect it is spam or a phishing email. Good inter-team collaboration and communication about this "password expiration notification process" cannot be emphasized enough.
    • Work with your helpdesk and security teams to ensure everyone signs off on this effort and approves the specific text and additional information for the email, including how to manage a 'reply' to that email address
  • When it all is working as desired/expected, you can disable testing:
    • $testing = "Disabled"

Now, at some pre-determined time, you or one of your staff can execute the script to generate the 'password expiry notification email' to the affected users.

For those who don't want to manually run the script, it's a simple process to create a Scheduled Task to run the script automatically. I would strongly suggest investigating the use of a Group Managed Service account rather than a traditional user account that runs a script or service, but that is a topic for another article.

There are numerous other ways to address this need; I have talked to many people who have developed their own processes, scripts and/or code for this. This particular process was pretty easy to implement, and I was able to work with my customers and my own lab environment to get the whole thing working in a short amount of time.

One last point of consideration would be to start moving away from passwords altogether. Think about multi-factor authentication (MFA), or passwordless solutions for an added bonus. One article that will help get you started can be found here: Azure Active Directory passwordless sign-in | Microsoft Docs.

Thanks again to David Morillo, Daniel Carroll, and everyone that responded to the original post to inspire this update.

 

See you all next time!

Mike "CANNONBALL!" Kullish

Michael Kullish -or- aka.ms/michaelkullish

 

 

 

 

Posted at https://sl.advdat.com/3AVaNB0