<# ============================================================================================= Name: Send Password Expiry Notifications to users in Microsoft 365 Version: 1.0 Website: o365reports.com Script Highlights: ~~~~~~~~~~~~~~~~~ 1. Sends password expiry notifications to users about upcoming password expiry. 2. Filters results to display licensed users alone. 3. Export a list of users who fall under the given criteria 4. Automatically install the Microsoft Graph PowerShell module (if not installed already) upon your confirmation. 5. The script can be executed with MFA and non-MFA accounts. 6. It can be executed with certificate-based authentication (CBA) too. 7. The script is schedular-friendly – automatically schedule the script in the task scheduler to automate sending password expiry notifications. For detailed Script execution: https://o365reports.com/2025/02/11/send-password-expiry-notification-in-microsoft-365/ ============================================================================================ #> Param ( [Parameter(Mandatory = $True)] [int]$DaysToExpiry, [Parameter(Mandatory = $false)] [switch]$LicensedUsersOnly, [switch]$Schedule, [string]$FromAddress, [string]$ClientId, [string]$TenantId, [string]$CertificateThumbprint, [Parameter(DontShow = $True)] [switch]$DoNotShowSummary ) $Date = Get-Date $CSVFilePath ="$(Get-Location)\PasswordExpiryNotificationSummary_$((Get-Date -format yyyy-MMM-dd-ddd` hh-mm` tt).ToString()).csv" [datetime]$StartTime = ($Date).AddDays(1).Date.AddHours(10) #Update the value of AddHours to change the Schedule start time. $ScriptPath = $MyInvocation.MyCommand.Path # Function to connect to Microsoft Graph function Connect_ToMgGraph { # Check if Microsoft Graph module is installed $MsGraphModule = Get-Module Microsoft.Graph -ListAvailable if ($MsGraphModule -eq $null) { Write-Host "`nImportant: Microsoft Graph module is unavailable. It is mandatory to have this module installed in the system to run the script successfully." $confirm = Read-Host "Are you sure you want to install Microsoft Graph module? [Y] Yes [N] No" if ($confirm -match "[yY]") { Write-Host "Installing Microsoft Graph module..." Install-Module Microsoft.Graph -Scope CurrentUser -AllowClobber Write-Host "Microsoft Graph module is installed in the machine successfully" -ForegroundColor Magenta } else { Write-Host "Exiting. `nNote: Microsoft Graph module must be available in your system to run the script" -ForegroundColor Red Exit } } Write-Host "`nConnecting to Microsoft Graph..." if (($TenantId -ne "") -and ($ClientId -ne "") -and ($CertificateThumbprint -ne "")) { # Use certificate-based authentication if TenantId, ClientId, and CertificateThumbprint are provided Connect-MgGraph -TenantId $TenantId -AppId $ClientId -CertificateThumbprint $CertificateThumbprint -NoWelcome } else { # Use delegated permissions (Scopes) if credentials are not provided Connect-MgGraph -Scopes "User.Read.All", "Domain.Read.All", "Mail.Send.Shared" -NoWelcome } # Verify connection if ((Get-MgContext) -ne $null) { if ((Get-MgContext).Account -ne $null) { Write-Host "Connected to Microsoft Graph PowerShell using account: $((Get-MgContext).Account)" } else { Write-Host "Connected to Microsoft Graph PowerShell using certificate-based authentication." } } else { Write-Host "Failed to connect to Microsoft Graph." -ForegroundColor Red Exit } } Connect_ToMgGraph if ((Get-MgContext).Account -ne $null){ if ([string]::IsNullOrEmpty($FromAddress)) { $FromAddress = (Get-MgContext).Account } } else { if ([string]::IsNullOrEmpty($FromAddress)) { Write-Host "`nError: FromAddress is required when using certificate-based authentication." -ForegroundColor Red Exit } } # Schedule Task if ($Schedule.IsPresent) { Write-Host "Configuring scheduled task to send password expiry notification..." # Validate mandatory parameters for scheduling if (-not $TenantId -or -not $ClientId -or -not $CertificateThumbprint) { Write-Host "`nError: TenantId, ClientId, and CertificateThumbprint are mandatory for scheduling the script." -ForegroundColor Red Exit } # Define the action and trigger for the schedule to execute the script $Action = New-ScheduledTaskAction -Execute "PowerShell.exe" -Argument "-File `"$($ScriptPath)`" -DoNotShowSummary -TenantId `"$($TenantId)`" -ClientId `"$($ClientId)`" -CertificateThumbprint `"$($CertificateThumbprint)`" -DaysToExpiry $($DaysToExpiry) -FromAddress `"$($FromAddress)`" -WindowStyle Hidden" $Trigger = New-ScheduledTaskTrigger -Daily -At $StartTime $Principal = New-ScheduledTaskPrincipal -UserId $env:UserName -LogonType Interactive -RunLevel Highest # Register the task $TaskName = "Password Expiry Notification" try { Register-ScheduledTask -Action $Action -Trigger $Trigger -Principal $Principal -TaskName $TaskName -Description "Runs the Password Expiry Notification script" -ErrorAction Stop | Out-Null Write-Host "`nScheduled task '$TaskName' created successfully and it will run daily at $($StartTime.ToString('hh:mm tt')) from $($StartTime.ToString('yyyy-MM-dd'))." -ForegroundColor Cyan } catch { Write-Host "`nError: Failed to register the scheduled task." -ForegroundColor Red Write-Host "Details: $_" -ForegroundColor Red Exit } } $Counter = 0 $PwdExpirigUsersCount = 0 $Domains = @{} Get-MgDomain | Select-Object Id, AuthenticationType, PasswordValidityPeriodInDays | ForEach-Object { if ($_.AuthenticationType -eq "Federated") { $Domains[$_.Id] = 0 } else { if ($_.PasswordValidityPeriodInDays -ne $null) { $Domains[$_.Id] = $_.PasswordValidityPeriodInDays } else { $Domains[$_.Id] = 90 } } } Get-MgUser -Filter "accountEnabled eq true" -All -Property AssignedLicenses, PasswordPolicies, DisplayName, UserPrincipalName, LastPasswordChangeDateTime | Where-Object {$_.PasswordPolicies -notcontains "DisablePasswordExpiration"} | ForEach-Object { $Name = $_.DisplayName $EmailAddress = $_.UserPrincipalName $LicenseStatus = $_.AssignedLicenses $PwdLastChangeDate = $_.LastPasswordChangeDateTime $UserDomain = $EmailAddress.Split('@')[1] $MaxPwdAge = $Domains[$UserDomain] $Counter++ Write-Progress -Activity "Processed Users: $($Counter)" -Status "Processing $($_.DisplayName)" if ($MaxPwdAge -ne 2147483647) { $ExpiryDate = $PwdLastChangeDate.AddDays($MaxPwdAge) $DaysToExpire = ($ExpiryDate.Date - $Date.Date).Days if ($LicenseStatus -ne $null) { $LicenseStatus = "Licensed" } else { $LicenseStatus = "Unlicensed" } if($DaysToExpire -eq 0){ $Msg = "Today" } elseif($DaysToExpire -eq 1){ $Msg = "Tomorrow" } else{ $Msg = "in " + "$DaysToExpire" + " days" } $params = @{ message = @{ subject = "Your Password is About to Expire – Update Required" body = @{ contentType = "HTML" content = "Hello $($Name),

Your Microsoft 365 account password will expire $($Msg).

To avoid disruptions in accessing Microsoft 365 apps and services, please update your password promptly by visiting the secure Microsoft Office portal.

If you have any questions or encounter issues, feel free to contact help desk for assistance.

Thank you for your attention to this matter.

Best regards,
IT Admin Team." } toRecipients = @( @{ emailAddress = @{ address = $EmailAddress } } ) } } [switch] $ProcessUser = $True if (($LicensedUsersOnly.IsPresent) -and ($LicenseStatus -ne "Licensed")) { $ProcessUser = $false } if ($ProcessUser.IsPresent) { if (($DaysToExpire -ge "0") -and ($DaysToExpire -le $DaysToExpiry)){ $PwdExpirigUsersCount++ Send-MgUserMail -UserId $FromAddress -BodyParameter $params $ExportResult = @{'Name' = $Name; 'Email Address' = $EmailAddress; 'Days to Expire' = $DaysToExpire; 'Password Expiry Date' = $ExpiryDate; 'License Status' = $LicenseStatus} $ExportResults = New-Object PSObject -Property $ExportResult $ExportResults | Select-object 'Name', 'Email Address', 'Days to Expire', 'Password Expiry Date', 'License Status' | Export-csv -path $CSVFilePath -NoType -Append -Force } } } } # Disconnect from Microsoft Graph Disconnect-MgGraph | Out-Null Write-Host `n~~ Script prepared by AdminDroid Community ~~`n -ForegroundColor Green Write-Host "~~ Check out " -NoNewline -ForegroundColor Green; Write-Host "admindroid.com" -ForegroundColor Yellow -NoNewline; Write-Host " to get access to 1900+ Microsoft 365 reports. ~~" -ForegroundColor Green if (!$DoNotShowSummary.IsPresent) { if ((Test-Path -Path $CSVFilePath) -eq "True") { Write-Host `n"$PwdExpirigUsersCount users' password is about to expire in $DaysToExpiry days, view there details in: " -NoNewline -ForegroundColor Yellow Write-Host $CSVFilePath $prompt = New-Object -ComObject wscript.shell $userInput = $prompt.popup("Do you want to open output files?", 0, "Open Output File", 4) if ($userInput -eq 6) { Invoke-Item "$CSVFilePath" } } else{ Write-Host `n"No user(s) found with passwords expiring in the next $DaysToExpiry days." } }