When Microsoft removed password expiration from Microsoft’s Cybersecurity Baseline recommendations, the password expiration notification email in Office 365 went along with it.
Office 365 administrators cannot set a password expiration notification for their tenants. This situation may be acceptable if they follow Microsoft’s recommendation to turn off password expiration. But administrators are left to fill the gap for those who opt to keep their password expiration policies.
Luckily, here comes PowerShell to the rescue!
Stay tuned, and we’ll explore how to send Office 365 password expiration notification emails to users using PowerShell and Microsoft Graph API.
Table of Contents
Requirements
- A computer with Windows PowerShell 5.1 or PowerShell Core (7.x). This post will use PowerShell 7.3.6, the latest version as of this writing.
- The Microsoft Graph PowerShell SDK must be installed on your computer. The latest version as of this writing is 2.2.0.
- A mailbox to use as the email notification sender. A shared mailbox with no license should be OK.
Prep Work: Register an App Principal in Azure AD
This step is crucial for automating the Office 365 password expiration notification as an unattended scheduled job. Running the task under your personal administrator credentials is not recommended because it risks exposure and will not work if the account is MFA-enabled.
But if you plan to manually run the Office 365 password expiration notification, you can skip this prep work.
Register a New Azure AD App
- Log in to your Microsoft Entra admin center.
- Navigate to Identity → Applications → App registrations → New registration.
- Under “Register an application”, fill out the form as follows:
- Name: “Office 365 Password Expiration Notification“
- Supported account types: “Accounts in this organizational directory only (Single tenant)”
- Redirect URI: “Web”, “http://localhost”
Once satisfied, click Register.
- Copy and record the Application (client) ID and Directory (tenant) ID.
Add API Permissions
- Once registered, click “API permissions” and “Add a permission.”
- Click the “Microsoft Graph” button.
- Select “Application Permissions.”
- Find and enable the following permissions: “User.Read.All”, “Mail.Send”, “Domain.Read.All”. Once you’ve selected the permissions, click the “Add permissions” button.
- Next, click the “Grant admin consent for ” button.
Note that only a Global Administrator can grant consent to the tenant. If you’re not a Global Administrator, ask someone else to grant this consent for you.
Notice the “Status” changes to “Granted”.
Add a Certificate
- Open PowerShell on your computer and run the following commands.
# Specify the app or certificate name $CertificateOrAppName = 'Office 365 Password Expiration Notification' # Generate a self-signed certificate $certSplat = @{ Subject = $CertificateOrAppName NotBefore = ((Get-Date).AddDays(-1)) NotAfter = ((Get-Date).AddYears(3)) CertStoreLocation = "Cert:\CurrentUser\My" Provider = "Microsoft Enhanced RSA and AES Cryptographic Provider" HashAlgorithm = "SHA256" KeySpec = "KeyExchange" KeyExportPolicy = "Exportable" } $selfSignedCertificate = New-SelfSignedCertificate @certSplat # Export the public certificate. $selfSignedCertificate | Export-Certificate -FilePath ".\$CertificateOrAppName.cer"
This command creates a new self-signed certificate in the Personal certificate store (Cert:\CurrentUser\My) and saves the public certificate in the current working directory. The private certificate (with the private key) remains in your Personal certificate store. Keep it secure.
Note the location of your certificate. You’ll need it later. - Click Certificates & secrets → Certificates → Upload certificate.
- Locate and select the public certificate and click Add.
- Once the certificate has been uploaded, copy and record the “Thumbprint” value. You’ll need this thumbprint to authenticate.
Add a Secret
You can also add a secret in place of a self-signed certificate. Note that certificate-based authentication is still the more secure method, but a secret-based credential is also valid.
- Under Certificates & Secrets, click Client secrets → New client secret.
- Enter the description and choose when the secret will expire. Click Add.
- Copy the secret value and store it as if it were a password.
Step 1: Connect to Microsoft Graph PowerShell
Assuming all requirements are in place, the first step is to connect to the Microsoft Graph PowerShell. The connection command varies based on the method to use.
# Using Delegated Access Connect-MgGraph -Scopes 'User.Read.All', 'Mail.Send', 'Domain.Read.All' # Use App-only access with a client secret credential. This method requires additional safeguards to avoid exposing the secret key. Connect-MgGraph -TenantId <tenant ID> -ClientSecretCredential <credential> # Use App-only access with a certificate Connect-MgGraph -ClientId <client ID> -TenantId <tenant ID> -CertificateThumbprint <thumbprint> Connect-MgGraph -ClientId <client ID> -TenantId <tenant ID> -Certificate (Get-Item CERT:\CurrentUser\My\<thumbprint>)
In this example, I’m using app-only access with a certificate.
Connect-MgGraph ` -TenantId 'c58f0e93-31cf-4c4c-8b73-e962e8503595' ` -ClientId '31e823b9-45d4-40fb-9b86-c571b0f24b0e' ` -CertificateThumbprint '056A40BC2B5ADE70DC09C89F2188D6C05EE34FCE'
Confirm the scopes (permissions) are correct.
Get-MgContext
Step 2: Retrieve the Password Expiration Settings of All Domains
In most cases, the password expiration setting for the tenant is uniform for every domain. But in some cases, each domain may have a different set of password expiration settings, and we have to take it into account.
First, let’s get the tenant domains to determine their password expiration age. This command excludes those domains whose PasswordValidityPeriodInDays value is 2147483647.
- If the PasswordValidityPeriodInDays = 2147483647, the passwords for that domain do not expire.
- If the PasswordValidityPeriodInDays is empty, the effective value is 90 days.
$domains = Get-MgDomain | Where-Object { $_.PasswordValidityPeriodInDays -ne 2147483647 } | Select-Object Id, PasswordValidityPeriodInDays $domains | ForEach-Object { if (!$_.PasswordValidityPeriodInDays) { $_.PasswordValidityPeriodInDays = 90 } } $domains
As you can see below, only one domain has the password expiration enabled (for 110 days).
Step 3: Get Office 365 Users Password Expiration Date
Let’s get all users’ password expiration dates and remaining days. We’ll do this by calculating the expected expiration date based on the PasswordValidityPeriodInDays of the user’s domain.
But first, let’s define which user properties to retrieve.
$properties = "UserPrincipalName", "mail", "displayName", "PasswordPolicies", "LastPasswordChangeDateTime", "CreatedDateTime"
Next, run this command to get all users, excluding those not enabled and whose PasswordPolicies value is not equal to DisablePasswordExpiration.
$users = Get-MgUser -Filter "userType eq 'member' and accountEnabled eq true" ` -Property $properties -CountVariable userCount ` -ConsistencyLevel Eventual -All -PageSize 999 -Verbose | ` Select-Object $properties | Where-Object { $_.PasswordPolicies -ne 'DisablePasswordExpiration' -and "$(($_.userPrincipalName).Split('@')[1])" -in $($domains.id) }
Next, let’s add more custom properties to the $users objects.
$users | Add-Member -MemberType NoteProperty -Name Domain -Value $null $users | Add-Member -MemberType NoteProperty -Name MaxPasswordAge -Value 0 $users | Add-Member -MemberType NoteProperty -Name PasswordAge -Value 0 $users | Add-Member -MemberType NoteProperty -Name ExpiresOn -Value (Get-Date '1970-01-01') $users | Add-Member -MemberType NoteProperty -Name DaysRemaining -Value 0
Now, let’s iterate through each user and populate the additional properties.
# Get the current datetime for calculation $timeNow = Get-Date foreach ($user in $users) { # Get the user's domain $userDomain = ($user.userPrincipalName).Split('@')[1] # Get the maximum password age based on the domain password policy. $maxPasswordAge = ($domains | Where-Object { $_.id -eq $userDomain }).PasswordValidityPeriodInDays # Skip the user if the PasswordValidityPeriodInDays is 2147483647, which means no expiration. if ($maxPasswordAge -eq 2147483647) { continue; } $passwordAge = (New-TimeSpan -Start $user.LastPasswordChangeDateTime -End $timeNow).Days $expiresOn = (Get-Date $user.LastPasswordChangeDateTime).AddDays($maxPasswordAge) $user.Domain = $userDomain $user.maxPasswordAge = $maxPasswordAge $user.passwordAge = $passwordAge $user.expiresOn = $expiresOn $user.daysRemaining = $( # If the remaining days are negative, show 0 instead. if (($daysRemaining = (New-TimeSpan -Start $timeNow -End $expiresOn).Days) -lt 1) { 0 } else { $daysRemaining } ) }
Finally, let’s display the password expiration dates for each user.
$users | Sort-Object DaysRemaining | Format-Table UserPrincipalName, DisplayName, Mail, PasswordAge, ExpiresOn, DaysRemaining
The result below shows each user, their password age, expiration date, and remaining days before expiration.
Step 4: Send the Office 365 Password Expiration Notification Email
Now that we have a list of users with their password expiration dates, it’s time to send them the notification email.
First, define which users will be notified based on how many days remain before their passwords expire. For example, notify users whose passwords will expire in 31, 17, 14, 10, 5, 3, and 1 days.
# Specify which days remaining will be notified. $PasswordNotificationWindowInDays = @(31, 17, 14, 10, 5, 3, 1)
Next, specify the email address that will be the notification sender. If you’re using app-only authentication, the sender can be any valid mailbox. If you’re using delegated authentication, the sender address must be your mailbox.
# Specify the sender's email address. $SenderEmailAddress = 'PasswordExpirationNotification@lazyexchangeadmin.cyou'
Finally, the code below will iterate through each user and send them the Office 365 password expiration notification email if they match the DaysRemaining threshold.
# Send Office 365 password expiration notification foreach ($user in $users) { # Guard clause if the user's DaysRemaining value is not within - # the $PasswordNotificationWindowInDays # and has no email address (can't send the user an email) if ($user.DaysRemaining -notin $PasswordNotificationWindowInDays -or !$user.Mail) { continue; } # Compose the message $mailBody = @() $mailBody += '<!DOCTYPE html><html><body>' $mailBody += "<p>Dear, $($user.DisplayName)</p>" $mailBody += "<p>The password for your Office 365 account ($($user.UserPrincipalName)) will expire on <b>$(get-date $user.ExpiresOn -Format D)</b>." $mailBody += "<br>Please change your password soon to avoid interruption to your access.</p>" $mailBody += "<p>Thank you. - The IT Team</p>" # Create the mail object $mailObject = @{ Message = @{ ToRecipients = @( @{ EmailAddress = @{ Address = $($user.Mail) } } ) Subject = "Your Office 365 password will expire in $($user.DaysRemaining) day(s)" Body = @{ ContentType = "HTML" Content = ($mailBody -join "`n") } } SaveToSentItems = "false" } # Send the Office 365 Password Expiration Notification Email try { "Sending password expiration notice to [$($user.displayName)] [Expires in: $($user.daysRemaining) days] [Expires on: $($user.expiresOn)]" | Out-Default Send-MgUserMail -BodyParameter $mailObject -UserId $SenderEmailAddress } catch { "There was an error sending the notification to $($user.displayName)" | Out-Default $_.Exception.Message | Out-Default } }
Wait until all notification emails are sent to matching users.
Checking the user’s mailbox, here’s the example Office 365 password expiration notification message.
Download the Script
For your convenience, the script we showcased in this post is available for download in this GitHub repository.
Conclusion
There’s no one-size-fits-all solution to sending Office 365 password expiration notification emails. Your requirements may be different, and that ok. You can take the script provided in this post and modify it to suit your needs. All we did was show you the fundamentals, and it’s up to you to make improvements.
2 comments
Hi,
Thanks for the script …
When i run my script there is no result, just a black screen like this:
PS C:\Tools\APP_Registration> .\Invoke-PasswordExpirationNotification.ps1
PS C:\Tools\APP_Registration>
Users are not receiving e-mail.
Is there any option to test this first by putting only one account in?
Thanks,
Rene
When you do Steps1 to 3, does the $users variable return a list of user with expiring passwords? If not, then its possible that your $PasswordNotificationWindowInDays values didn’t match any users’ DaysRemaining result.