Update
Added an update to this regarding secure authentication: https://www.smthwentright.com/2022/04/03/password-reminder-with-proactive-remediation-for-aad-joined-devices-update-using-azure-functions-for-a-more-secure-way-to-call-the-enterprise-application/
Introduction
With Azure AD Joined devices the end user no longer gets notification of expiring passwords as we might be used to when having AD joined devices. If we have an environment with AD Synced accounts with password change enforced after e.g 3 months and Azure AD Joined devices managed with Intune this might create some issues for the end user as their password expires and authentication is still cached for some authentications but might not be for others this often results in end users having to create a support ticket.
Solution
There are multiple ways to go about addressing this and I’m by no way saying this is the best way of accomplishing a ‘password is about to expire’ notification for the end user. Besides using Proactive remediation’s I’ve previously used Azure Automation account to send an email to users that have passwords about to expire. But I found that using an email to encourage the user to change password might be go against IT policy’s way of providing information to end users and therefore be unsafe.
So the Idea of using a Password Reminder with Proactive Remediation for me actually stems from trying to accomplishing different types of notification for end users with Toasts years ago using an RMM system, back then I first tried leveraging Burnt Toast (https://github.com/Windos/BurntToast) a very cool PowerShell module, feel free to check it out. I picked up the idea again when I saw Martin Bengtsson at imab.dk utilizing a toast to notify end users about needing to restart and password expiration. So with very little modification we could simplify it and run it with Intune.
Note:
The use of this solution should hopefully be obsolete soon enough with the enterprise world moving towards a more secure policy where users isn’t enforced to change password and further along passwordless authentication.
What we need
- An Enterprise Application
- We need to be able to read how long it was since the user set his last Password
- A Detection Script
- This will Authenticate to Azure AD using the Enterprise Application and “Calculate” (Note the quotation marks on Calculate, will expand on this later)
- A Remediation Script
- This will be what actually creates the notification if the user is to have his password expire
- Some input information in our Script
- Title, Text & Possible Image for the Notification
Enterprise Application
Firstly we need the Enterprise application, this will be used to authenticate against the Azure AD and read how long it was since the user last set his password.
- Open Azure AD
- Navigate to App registrations
- Select New registration
- Select Accounts in this organizational directory only
- Select a fitting Name for your application, I chose “IntunePasswordNotification” but it doesn’t matter
- Register
When the Application is finished creating we need to make Note of the Application ID and the Tenant ID visible on the Overview tab
Now we need to assign the permissions we need for the Application to be able to read the Password age of the users.
Navigate to the API permissions tab
- Select Add a permission
- Chose Microsoft Graph
- Chose Application permissions
- Search User.Read.All
- Mark the box for User.Read.All under User
- Add permissions
- Review that the correct permissions have been granted then Select Grant admin consent for “Tenant”
Now we just need to create a way for us to authenticate against the Application, navigate to the Certificates & secrets tab
- Make sure you have Client secrets highlighted
- Select New client secret
- Type a descriptive name for the secret and select an expiration, I chose 12 months and entered “Proactive Remediation secret” in the description but it doesn’t matter
- Add
- Make note of the Value, Secret ID, Description & Expiration date
- This will be the only time the Value is visible, after leaving this tab the secret is gone forever
- Enter the information in e.g a Password manager solution for safe keeping
Detection Script
(Scripts at the end)
First we need to look at the detection script, this is what determines whether or not to execute the remediation script. In our case the detection script will check if the User password is about to expire and the remediation script will trigger the notification.
Now we need to input the information we gathered from the Enterprise Application into the script so it can fetch the last time the password was changed and using a manual input it will calculate when the password is about to expire.
The variables we need to change are located at the top of the script. The variable for $PasswordExpirationDays should be how long your password expiration policy is in days.
1 2 3 4 5 |
$TenantID = "Enter Tenant ID" $ApplicationID = "Enter Application ID" $AppSecret = 'Enter Secret Value' $PasswordExpirationDays = 90 |
The notification will appear every time the Proactive Remediation runs and the password is about to expire in less than 10 days, this can also be changed with the lines
1 2 3 4 |
If (($TimeSpan.Days -le 10) -and ($TimeSpan.Days -ge -5)) { Write-Output "Password Expires after $($TimeSpan.Days) days" Exit 1 } |
The condition for -5 is to fix some issues with e.g users that has Password Never expires.
After changing the variables save the script as something along the lines of “Detection Script password Notification” (or whatever that helps you know this is the detection script)
Remediation Script
(Scripts at the end)
Now the remediation script is a bit more complicated but still pretty straight forward. The variables we need to change here determines the Title, Text, Image and Action to be taken when the user selects “Change Password”.
The current Action is set to open the Azure AD change password page but can be changed to a Sharepoint document or whatever your prefer.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
$TitleText = "Password about to Expire" $BodyText1 = "Your Password is about to expire" $BodyText2 = "If you do not reset your password within ten days from when this message is displayed the first time your account will be locked. If you are unable to reset your password or your password has already been reset, please contact the support." $HeaderText = "EMS" #Remove this if you want to use an URL and your own image instead $HeroImagePath = "C:\Windows\Web\Wallpaper\Theme1\img1.jpg" #If you want to you use your own URL use theese variables instead, also uncomment line 100 & 101 #$HeroImageFile = "Paste URL here if you want to download your own image from e.g an Azure Storage Account" #$HeroImageName = "img1.jpg" $Action = "https://account.activedirectory.windowsazure.com/ChangePassword.aspx" |
There are also several more parts of this Script that can be changed if for preference like the Company Portal logo and the Attribute text as well as the button text
Proactive Remediation
Now to put it all together and start using the Proactive Remediation to deploy the notification.
- Open Endpoint Manager
- Navigate to Reports
- Navigate to Endpoint analytics
- Navigate to Proactive remediation’s
- Create script package
- Select a fitting Name, I chose “Password Notification”
- Upload your detection script & Remediation Script
- Run this script using the logged-on credentials -> Yes
- Enforce script signature check -> No
- Run script in 64-bit PowerShell -> Yes
- Assign to a User group and Assign it to run Daily
Done!
Try it out, tinker with it how you like and if you have any way to improve please feel free to comment
Further
So this is definitely something that can be improved upon in multiple ways.
One of the first ways to improve would be to create a proper calculation of the password expiration instead of using the stupid solution I have for it now where you input the amount of days the policy is and then just fetching the last time for password change. Also might be able to improve upon the AAD Application with using permissions restricted to the last password change instead of full profile.
I would be ever so grateful for feedback, comments or ideas how to improve upon this further
Detection Script
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 |
$TenantID = "Enter Tenant ID" $ApplicationID = "Enter Application ID" $AppSecret = 'Enter Secret Value' $PasswordExpirationDays = 90 $WindirTemp = Join-Path $Env:Windir -Childpath "Temp" $UserTemp = $Env:Temp $UserContext = [Security.Principal.WindowsIdentity]::GetCurrent() Switch ($UserContext) { { $PSItem.Name -Match "System" } { Write-Output "Running as System" ; $Temp = $UserTemp } { $PSItem.Name -NotMatch "System" } { Write-Output "Not running System" ; $Temp = $WindirTemp } Default { Write-Output "Could not translate Usercontext" } } $logfilename = "PasswordNotificationDS" $logfile = Join-Path $Temp -Childpath "$logfilename.log" $LogfileSizeMax = 100 ############################## ## Functions ############################## Function Get-MSGraphAuthToken { [cmdletbinding()] Param( [parameter(Mandatory = $true)] [pscredential]$Credential, [parameter(Mandatory = $true)] [string]$tenantID ) #Get token $AuthUri = "https://login.microsoftonline.com/$TenantID/oauth2/token" $Resource = 'graph.microsoft.com' $AuthBody = "grant_type=client_credentials&client_id=$($credential.UserName)&client_secret=$($credential.GetNetworkCredential().Password)&resource=https%3A%2F%2F$Resource%2F" $Response = Invoke-RestMethod -Method Post -Uri $AuthUri -Body $AuthBody If ($Response.access_token) { return $Response.access_token } Else { Throw "Authentication failed" } } Function Invoke-MSGraphQuery { [CmdletBinding(DefaultParametersetname = "Default")] Param( [Parameter(Mandatory = $true, ParameterSetName = 'Default')] [Parameter(Mandatory = $true, ParameterSetName = 'Refresh')] [string]$URI, [Parameter(Mandatory = $false, ParameterSetName = 'Default')] [Parameter(Mandatory = $false, ParameterSetName = 'Refresh')] [string]$Body, [Parameter(Mandatory = $true, ParameterSetName = 'Default')] [Parameter(Mandatory = $true, ParameterSetName = 'Refresh')] [string]$token, [Parameter(Mandatory = $false, ParameterSetName = 'Default')] [Parameter(Mandatory = $false, ParameterSetName = 'Refresh')] [ValidateSet('GET', 'POST', 'PUT', 'PATCH', 'DELETE')] [string]$method = "GET", [Parameter(Mandatory = $false, ParameterSetName = 'Default')] [Parameter(Mandatory = $false, ParameterSetName = 'Refresh')] [switch]$recursive, [Parameter(Mandatory = $true, ParameterSetName = 'Refresh')] [switch]$tokenrefresh, [Parameter(Mandatory = $true, ParameterSetName = 'Refresh')] [pscredential]$credential, [Parameter(Mandatory = $true, ParameterSetName = 'Refresh')] [string]$tenantID ) $authHeader = @{ 'Accept' = 'application/json' 'Content-Type' = 'application/json' 'Authorization' = "Bearer $Token" } [array]$returnvalue = $() Try { If ($body) { $Response = Invoke-RestMethod -Uri $URI -Headers $authHeader -Body $Body -Method $method -ErrorAction Stop } Else { $Response = Invoke-RestMethod -Uri $URI -Headers $authHeader -Method $method -ErrorAction Stop } } Catch { If (($Error[0].ErrorDetails.Message | ConvertFrom-Json -ErrorAction SilentlyContinue).error.Message -eq 'Access token has expired.' -and $tokenrefresh) { $token = Get-MSGraphAuthToken -credential $credential -tenantID $TenantID $authHeader = @{ 'Content-Type' = 'application/json' 'Authorization' = $Token } $returnvalue = $() If ($body) { $Response = Invoke-RestMethod -Uri $URI -Headers $authHeader -Body $Body -Method $method -ErrorAction Stop } Else { $Response = Invoke-RestMethod -Uri $uri -Headers $authHeader -Method $method } } Else { Throw $_ } } $returnvalue += $Response If (-not $recursive -and $Response.'@odata.nextLink') { Write-Warning "Query contains more data, use recursive to get all!" Start-Sleep 1 } ElseIf ($recursive -and $Response.'@odata.nextLink') { If ($PSCmdlet.ParameterSetName -eq 'default') { If ($body) { $returnvalue += Invoke-MSGraphQuery -URI $Response.'@odata.nextLink' -token $token -body $body -method $method -recursive -ErrorAction SilentlyContinue } Else { $returnvalue += Invoke-MSGraphQuery -URI $Response.'@odata.nextLink' -token $token -method $method -recursive -ErrorAction SilentlyContinue } } Else { If ($body) { $returnvalue += Invoke-MSGraphQuery -URI $Response.'@odata.nextLink' -token $token -body $body -method $method -recursive -tokenrefresh -credential $credential -tenantID $TenantID -ErrorAction SilentlyContinue } Else { $returnvalue += Invoke-MSGraphQuery -URI $Response.'@odata.nextLink' -token $token -method $method -recursive -tokenrefresh -credential $credential -tenantID $TenantID -ErrorAction SilentlyContinue } } } Return $returnvalue } ############################## ## Scriptstart ############################## If ($logfilename) { If (((Get-Item -ErrorAction SilentlyContinue $logfile).length / 1MB) -gt $LogfileSizeMax) { Remove-Item $logfile -Force } Start-Transcript $logfile -Append | Out-Null Get-Date } #[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 Try { $LoggedSID = Get-WmiObject -Class win32_computersystem | Select-Object -ExpandProperty Username | ForEach-Object { ([System.Security.Principal.NTAccount]$_).Translate([System.Security.Principal.SecurityIdentifier]).Value } } Catch { Write-Error -Message "Failed to gather SID for current user" -ErrorAction Stop } Try { $CurrentAzureADUser = (Get-ItemProperty -ErrorAction SilentlyContinue -Path "HKLM:\SOFTWARE\Microsoft\IdentityStore\Cache\$LoggedSID\IdentityCache\$LoggedSID" -Name UserName).UserName } Catch { Write-Error -Message "Failed to gather CurrentAzureADUser" -ErrorAction Stop } If (!($CurrentAzureADUser)) { Write-Output "Failed to gather CurrentAzureADUser, Exiting" ; Exit 0 } $Credential = New-Object System.Management.Automation.PSCredential($ApplicationID, (ConvertTo-SecureString $AppSecret -AsPlainText -Force)) $Token = Get-MSGraphAuthToken -credential $Credential -TenantID $TenantID $UserName = $CurrentAzureADUser $resourceURL = "https://graph.microsoft.com/v1.0/users/$UserName`?`$select=userprincipalname,lastPasswordChangeDateTime" $User = Invoke-MSGraphQuery -method GET -URI $resourceURL -token $token #$Date = Get-Date -format "yyyy-MM-dd hh:mm:ss" [datetime]$lastpasswordChange = $User.lastPasswordChangeDateTime -replace "T", " " -replace "Z","" $PasswordExpirationDate = ($lastpasswordChange).AddDays($PasswordExpirationDays) $StartDate = (Get-Date).ToString("yyyy-MM-dd HH:mm:ss") $TimeSpan = New-Timespan -Start $StartDate -End $PasswordExpirationDate If (($TimeSpan.Days -le 10) -and ($TimeSpan.Days -ge -5)) { Write-Output "Password Expires after $($TimeSpan.Days) days" Exit 1 } If ($logfilename) { Stop-Transcript | Out-Null } Exit 0 |
Remediation Script
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 |
$TitleText = "Password about to Expire" $BodyText1 = "Your Password is about to expire" $BodyText2 = "If you do not reset your password within ten days from when this message is displayed the first time your account will be locked. If you are unable to reset your password or your password has already been reset, please contact the support." $HeaderText = "EMS" #Remove this if you want to use an URL and your own image instead $HeroImagePath = "C:\Windows\Web\Wallpaper\Theme1\img1.jpg" #If you want to you use your own URL use theese variables instead, also uncomment line 100 & 101 #$HeroImageFile = "Paste URL here if you want to download your own image from e.g an Azure Storage Account" #$HeroImageName = "img1.jpg" $Action = "https://account.activedirectory.windowsazure.com/ChangePassword.aspx" $WindirTemp = Join-Path $Env:Windir -Childpath "Temp" $UserTemp = $Env:Temp $UserContext = [Security.Principal.WindowsIdentity]::GetCurrent() Switch ($UserContext) { { $PSItem.Name -Match "System" } { Write-Output "Running as System" ; $Temp = $UserTemp } { $PSItem.Name -NotMatch "System" } { Write-Output "Not running System" ; $Temp = $WindirTemp } Default { Write-Output "Could not translate Usercontext" } } $logfilename = "PasswordNotificationRE" $logfile = Join-Path $Temp -Childpath "$logfilename.log" $LogfileSizeMax = 100 ############################## ## Functions ############################## function Test-WindowsPushNotificationsEnabled() { $ToastEnabledKey = (Get-ItemProperty "HKCU:\Software\Microsoft\Windows\CurrentVersion\PushNotifications" -Name ToastEnabled -ErrorAction Ignore).ToastEnabled if ($ToastEnabledKey -eq "1") { Write-Output "Toast notifications are enabled in Windows" return $true } elseif ($ToastEnabledKey -eq "0") { Write-Output "Toast notifications are not enabled in Windows. The script will run, but toasts might not be displayed" return $false } else { Write-Output "The registry key for determining if toast notifications are enabled does not exist. The script will run, but toasts might not be displayed" return $false } } function Display-ToastNotification() { $Load = [Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] $Load = [Windows.Data.Xml.Dom.XmlDocument, Windows.Data.Xml.Dom.XmlDocument, ContentType = WindowsRuntime] # Load the notification into the required format $ToastXml = New-Object -TypeName Windows.Data.Xml.Dom.XmlDocument $ToastXml.LoadXml($Toast.OuterXml) # Display the toast notification try { Write-Output "All good. Displaying the toast notification" [Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier($App).Show($ToastXml) } catch { Write-Output "Something went wrong when displaying the toast notification" Write-Output "Make sure the script is running as the logged on user" } if ($CustomAudio -eq "True") { Invoke-Command -ScriptBlock { Add-Type -AssemblyName System.Speech $speak = New-Object System.Speech.Synthesis.SpeechSynthesizer $speak.Speak($CustomAudioTextToSpeech) $speak.Dispose() } } } function Test-NTSystem() { $currentUser = [Security.Principal.WindowsIdentity]::GetCurrent() if ($currentUser.IsSystem -eq $true) { $true } elseif ($currentUser.IsSystem -eq $false) { $false } } ##############################s ## Scriptstart ############################## If ($logfilename) { If (((Get-Item -ErrorAction SilentlyContinue $logfile).length / 1MB) -gt $LogfileSizeMax) { Remove-Item $logfile -Force } Start-Transcript $logfile -Append | Out-Null Get-Date } #$HeroImagePath = Join-Path -Path $Env:Temp -ChildPath $HeroImageName #If (!(Test-Path $HeroImagePath)) { Start-BitsTransfer -Source $HeroImageFile -Destination $HeroImagePath } ##Setting image variables $LogoImage = "" $HeroImage = $HeroImagePath $RunningOS = Get-CimInstance -Class Win32_OperatingSystem | Select-Object BuildNumber $isSystem = Test-NTSystem if ($isSystem -eq $True) { Write-Output "Aborting script" Exit 1 } $WindowsPushNotificationsEnabled = Test-WindowsPushNotificationsEnabled $PSAppStatus = "True" if ($PSAppStatus -eq "True") { $RegPath = "HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Notifications\Settings" $App = "Microsoft.CompanyPortal_8wekyb3d8bbwe!App" if (-NOT(Test-Path -Path "$RegPath\$App")) { New-Item -Path "$RegPath\$App" -Force New-ItemProperty -Path "$RegPath\$App" -Name "ShowInActionCenter" -Value 1 -PropertyType "DWORD" } if ((Get-ItemProperty -Path "$RegPath\$App" -Name "ShowInActionCenter" -ErrorAction SilentlyContinue).ShowInActionCenter -ne "1") { New-ItemProperty -Path "$RegPath\$App" -Name "ShowInActionCenter" -Value 1 -PropertyType "DWORD" -Force } } $AttributionText = "Information" $ActionButtonContent = "Change Password" $DismissButtonContent = "Remind me later" $CustomAudio = "False" $CustomAudioTextToSpeech = $Xml.Configuration.Option | Where-Object {$_.Name -like 'CustomAudio'} | Select-Object -ExpandProperty 'TextToSpeech' $Scenario = "Reminder" # Formatting the toast notification XML # Create the default toast notification XML with action button and dismiss button [xml]$Toast = @" <toast scenario="$Scenario"> <visual> <binding template="ToastGeneric"> <image placement="hero" src="$HeroImage"/> <image id="1" placement="appLogoOverride" hint-crop="circle" src="$LogoImage"/> <text placement="attribution">$AttributionText</text> <text>$HeaderText</text> <group> <subgroup> <text hint-style="title" hint-wrap="true" >$TitleText</text> </subgroup> </group> <group> <subgroup> <text hint-style="body" hint-wrap="true" >$BodyText1</text> </subgroup> </group> <group> <subgroup> <text hint-style="body" hint-wrap="true" >$BodyText2</text> </subgroup> </group> </binding> </visual> <actions> <action activationType="protocol" arguments="$Action" content="$ActionButtonContent" /> <action activationType="system" arguments="dismiss" content="$DismissButtonContent"/> </actions> </toast> "@ Display-ToastNotification If ($logfilename) { Stop-Transcript | Out-Null } Exit 0 |
Great script.
Works great for Azure joined only but does not seem to work on Hybrid joined devices.
Have you seem that by chance?
Do you know a way to also make this work for Hybrid joined devices?
Hi David!
New to the blog thing so I didn’t know I had to “accept” comments and so I missed your comments.
Sorry for the delay in Response, it should be possible with the Hybrid joined devices as well if you want to implement it there. Might need to change the method of fetching the correct username.
If you have a case we could look at it together if you want to
Terrific post! I don’t have any prior domain knowledge, but this is obviously the best solution to the problem.
Hi, it’s working as charm but one thing is it’s doesn’t show ÅÄÖ those letters. only english letters working, how to make this work. and i tried to put picture there
$HeroImagePath = “https://windows10spotlight.com/wp-content/uploads/2018/08/3514a0adfb1d9d72c64dd7cd03fdf99e.jpg” as we don’t have azure storage blob, picture doesn’t shows there either.
Last and not least
Many thanks for this amazing work
Great script, truly amazing.
ÅÄÖ letters not showing correctly as only english letters working on reminder and how to make this work with åäö those letters as a message shows to users. i also did this
$HeroImagePath = “https://windows10spotlight.com/wp-content/uploads/2018/08/3514a0adfb1d9d72c64dd7cd03fdf99e.jpg” as we don’t have Azure blob storage and i want to show this picture instead.
Many thanks for this charming work
Hi!
Regarding special letters you can do a base64 conversion to make it work
##USE THIS CODE HERE TO CREATE A BASE 64 STRING BUT DONT INCLUDE IN SCRIPT
<# [string]$sStringToEncode= "ÅÄÖ" $Base64Encode=[Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($sStringToEncode)) $Base64Encode #>
$Base64EncodeString = ""
$Base64Text = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($Base64EncodeString))
$BodyText1 = $Base64Text
Use the top part to convert the special letters to create a string, then paste the string to the variable $Base64EncodeString and then change the bottom variable that currently is $BodyText1 = $Base64Text to whatever variable you want to use special letters in.
To use another picture just uncomment the the code at lines 112 and 113.
#$HeroImagePath = Join-Path -Path $Env:Temp -ChildPath $HeroImageName
#If (!(Test-Path $HeroImagePath)) { Start-BitsTransfer -Source $HeroImageFile -Destination $HeroImagePath }
Let me know if anything was unclaer.
You are a diamond for this write up. We are ridding ourselves of Hybrid setups and we need users to reset before expiry. Your write up and sincerely appreciated. Can’t wait to have a test. Thanks 👍🏽
You are a diamond for this write up. We are ridding ourselves of Hybrid setups and we need users to reset before expiry. Your write up is sincerely appreciated. Can’t wait to have a test. Thanks 👍🏽
Thanks man!
This is an awesome Solution.
I was able to deploy this to my test group with no issues.
The only issue i found is that the expiration notification was early.
My test users account is not set to expire for another 42 days, but he received the notification today.
What should i look for to fix this?
thanks
Hi Viktor
This is an awesome script. My organization needed this in our intune environment.
I deployed this script and assigned it to my test group and I set it to check every day. but it notified the user that his password was expiring 32 days too early. He had 42 days before his password expires.
I didn’t make any changes to the script.
Can you advise, what I should look for or edit?
Thanks
Hi George! Sorry for the late reply.
Of course we can make this work together,
So right at the top of the “Detection script” you input the amount of days before a password expires in your environment, so this needs to match your environment. By standards its 90 days, but if you have 160 or something the calculations will come out wrong, make sure this is set correctly!
Hi George, did you find a solution for this? I’m facing the same issue and have $PasswordExpirationDays = 90 and If (($TimeSpan.Days -le 10) -and ($TimeSpan.Days -ge -5)) {
Write-Output “Password Expires after $($TimeSpan.Days) days”
Exit 1
Hi Viktor, appreciate your effort on providing this amazing script.
But I am facing an issue and I don’t know if you can provide any insight.
We are using multiple domains in our environment but users with different domain than our primary one get the notification constantly popping up even though they have successfully changed their password.
Hi Chris!
Sorry for the late reply, this is actually a very interesting problem.
Its hard for me to determine what might go wrong just based on this response,
But if you start a chat with me on Reddit we can come to a solution together with some logs and more info provided?
https://www.reddit.com/user/IntRangeNoShut
We too have started getting this repeated notification but it is now October. Did you all ever come up with a solution on what was causing this back in July?
Hi JHW, sorry for the late reply. Unfortunatly Chris never did get back to me, but if you want to troubleshoot this issue I would be happy to help. I think the easiset way is to start a chat with me and we’ll get it sorted: https://www.reddit.com/user/IntRangeNoShut
I’ve create a similar kind of pro-active remediation script but it queries the on-prem AD for password age and expiration (leveraging the client VPN connection) as we use PTA for authentication with Azure AD.
Wanna share your solution?
Hello,
Thanks for the script.
But what does the -5 stand for in the code:
If (($TimeSpan.Days -le 10) -and ($TimeSpan.Days -ge -5))
Hi!
No problem!
So that might look a bit janky. But the reason we need that is if has password not expired the notification will keep appearing for that person.
Now, you might remove it but might cause issues on some users if their password should have expired some time ago
Kind Regards, Viktor
Also, what I think is going to cause a problem is that in Europe date time is dd-MM-yyy.
I changed in the script on line 188:
$StartDate = (Get-Date).ToString(“dd-MM-yyy HH:mm:ss”)
Hope it will work like this. Cause changing the end-devices date time to yyy-MM-dd isn’t an option I’m afraid.
Hi!
Great that you pointed it out, if you don’t get it to work please let me know and I’ll help you get going
Hey Viktor,
Hoping to test this out with a test user account. Unfortunately, my test user PW was recently updated and the organization has a 180 day expiry policy. Is there any way that I can simulate an upcoming PW expiration with this? I also noticed in initial tests that I would get an account authentication prompt for PowerShell scripts on my test device. Do you know why this might be? It seems the calls were being made and the scripts run but even when I authenticated, nothing happened.
Thanks!
Hi Ian, sorry for the late reply. Of course we can simulate this, so if you create a test group and input your user there you can simply change the detection script to notify on 180 days. This is done at line 188 in the detection script:
If (($TimeSpan.Days -le CHANGE THIS NUMBER TO 180 OR WHATEVER) -and ($TimeSpan.Days -ge -5)) {
Write-Output “Password Expires after $($TimeSpan.Days) days”
Exit 1
}
You should definetly not get a authentication prompt, which script gave you this?
Best regards, Viktor
Hello!
Thanks so much for putting this together! It’s totally saved us as we transition to Cloud based management. That said, sometimes one of the scripts fires off erroneously and, not being good at code, wonder if there’s a way to avoid it.
It seems that when the script can’t resolve the hostname “(Invoke-RestMethod): “The remote name could not be resolved ‘login.Microsoft.com’” it writes Authentication Failed and then fires off the toast notification to change my password. This happened for a coworker of mine in testing and just happened for me today despite having just updated our passwords. I’m not sure if this is due to the script running at boot if it missed while on, and since the network takes a minute to connect to, it’s unreachable? Either way, is there a way to prevent the toast from firing when this specific error occurs? Thanks!
Hi Craig!
Thanks for your kind words. So I’m not entirely sure whats happening but it seems as the DNS doesnt resolve correctly and your theory about network might be correct. So what I think is going on with the script actually firing is that a proactive remediation is set to execute if the detection script returns an error. So to get around this we could simply introduce a Try – Catch and not output an error. Could you provide at what line the error occurs? There should be a log file under “AppData\Temp” for the user or “C:\Windows\Temp” called “PasswordNotificationDS.log”. Scroll down to the bottom of the file and post what is says
Hi Viktor,
Thanks for this fantastic script. Last week my customer asked for this which the users do not get.
From the “PasswordNotificationDS” log file, get and error “Failed to gather CurrentAzureADUser, Exiting”
Anything I need to look at?
Hi, if it fails to gather the current azure ad user its probably an error with the enterprise application. Did you grant permissions?
Viktor, signed up just to able to send a HUGE Thank You!! this script worked amazing and unlocked an area in Intune I was unaware existed.
I ended adjusting the “IF (($TimeSpan.Days -le 10) -and ($TimeSpan.Days -ge -5))” because I have users who password age was way older than 5 days and alert werenot triggering.
aside from that, the script works as expected.
Hi,
Thanks!! Great to hear you enjoyed it and thanks for sharing your adjustments!
Best regards, Viktor
Hi,
Looking for some info, maybe I missed it, but I’m wondering what exactly, or where in the script it is defined, what clicking the “Remind me later” button does. Also, if you click the “Change password” button but don’t follow through with the password change, will the prompt return?
Still in testing…
Carl
Hi! The button Remind me later just dismisses the notification and the user will get a new prompt the next day. If the user doesnt change the password the prompt will continue, execution is only determined by last password change time.
Let me know how it goes!
Regards, Viktor
Hi,
Working prefect. one issue is that i see 11 users with ” With issues” and “Recurred” what does it mean, and they didnt recieved the notification and one of those 11 got so many notification even thought password changed.
Any idea why
Best redagrs
Hi faced the same error.
Deployed to 5 users all status has “With Issues” and “Recurred”. Futher check has and error related to value “” to Date.time or something. i dont have the error message right now. i’ll update once i have the message
Hi Robinson, facing the same error.
Were you able to resolve this?
Hi Robinson,
did you find a solution for this? I’m facing the same issue
Hi Robinson, did you find a solution for this? I’m facing the same issue
Hi David, facing the same error.
Were you able to resolve this? Do you remember what was the cause?
Hey Viktor,
You might get a kick out of this. I had quickly skimmed your previous article and missed the part about the function app update. Well, I was intimately familiar with the Msendpoingmgr function app already thanks to my work on log analytics, and I ended up basically re-inventing this same wheel. I started making my own blog on how I did it, went back to your blog to give credit, and then I saw the link here. Whoops!
But, I still ended up posting it since mine isn’t quite the same and I think you might like some of the ways I went about things. Would love your feedback! I just got into the blogging side of things.
https://azuretothemax.net/2023/02/10/windows-toast-notification-based-password-expiration-reminders/
Awesome work Viktor. I’m looking at using a different approach for tagging users. What I’m thinking is using a updating a standard or custom attribute in the account, then using dynamic groups to filter for and assign the remediation scripts to the users.
So the detection process would be done via scheduled task on ADDC (for on-prem AD) or via Power-Automate/AzureLogic Apps (for AzAD; filtering accounts by “On-premises sync enabled”).The process(es) would update the attribute. Dynamic group query would add the user to the group (eg. Password Expired, PW expires in 10 days). Intune can apply the remediation script to the specific group of users. … also now we can quickly see or export the list of users with expired PWs.
Thoughts?
Hi
I am getting error message while run this script and error message is “Failed to gather CurrentAzureADUser, Exiting”.
I am looking same solutions for azure ad users
Hello Viktor,
How can we update that script to block users from Sign In if the password was not changed and expired?
Thank you.
I don’t think this is the best idea, disabling their account can be easily done via the graph API. But this also stops them from resetting their password as the entire account is disabled in AzureAD. If I find out a way to do this post in here
Does the $PasswordExpirationDays = 90 target the 365/Azure password policy? The one from either set-msol passwordpolicy, Update-MgDomain, 365 Admin Center?
I am currently in process of migrating user computers from on-prem AD to AADJ (not hybrid). But the user accounts are still syncing from on-prem AD to Azure. So on-prem AD is where GPO password policy originates. We actually never set 365/Azure password policy to expire. It’s still set to never expire.
However, I have already set enforcecloudpasswordpolicyforpasswordsyncedusers. Then I also marked couple test users Azure password policy to “none.” So they don’t use the 365/Azure password policy. Users are automatically set to none if they change their passwords.
On-prem AAD syncCompanyFeature also set to passwordhashsync, forcepasswordchangeonlogon.
SSPR already setup.
So my question is still, will this Toast notification work if the user account password policy comes from on-prem AD, if the AAD password policy is set to never expire, but the individual users are set to none. Or should I also set my 365/Azure password policy to match my on-prem policy?
I created app reg, scripts, and deployed thru remediation scripts. Set to hourly for now. But I don’t think toast notification is working.
Ignore my previous post. I see how it works now. And it is working. I tested by changing the notification to 90 days. to match the expiration policy.
If (($TimeSpan.Days -le 90) -and ($TimeSpan.Days -ge -5)) {
Write-Output “Password Expires after $($TimeSpan.Days) days”
Exit 1
}
However, I do have another question. How can I use this script for force password change next login? If the on-prem AD flag is enabled, then Azure will show the ForceChangePasswordNextLogin set to True.
Get-AzureADUser -ObjectID steve.rogers@mydomain.com | Select PasswordPolicies, PasswordProfile | fl
PasswordPolicies : None
PasswordProfile : class PasswordProfile {
Password:
ForceChangePasswordNextLogin: True
EnforceChangePasswordPolicy: False
}
I would like to use the remediation script to activate the toast notification.
Create a new detection script of ForceChangePasswordNextLogin is True, then use the remediation script to activate the toast notification.
This is no longer available in Endpoint Analytics and now requires an E3 or higher license. Has anyone come up with an alternate solution?
Trying to figure out how to implement a variable that tells the user how many days are remaining until their password expires in the remediation script. E.g. Dear User, your password is about to expire in $($TimeSpan.Days) days. However, since the TimeSpan is defined in the detection script, how would I get that value into the remediation?
Same user here. Disregard, I have found the solution.
Hello there,
I have the same problem and my solution is to write the expiration date in the HKU and then read it in the remediation script. Here is what I do:
$loggedonuser = ((Get-WMIObject -ClassName Win32_ComputerSystem).Username).Split(‘\’)[1]
# Gets the USID of the current logged on user
$currentusersid = Get-WmiObject -Class win32_computersystem | Select-Object -ExpandProperty Username | ForEach-Object { ([System.Security.Principal.NTAccount]$_).Translate([System.Security.Principal.SecurityIdentifier]).Value }
# Add password expiration date to registry HKCU:\Software\
$RegKey = Get-ItemPropertyValue -Path “REGISTRY::HKEY_USERS\$currentusersid\SOFTWARE\” -Name PasswordExpiration
if ($null -ne $RegKey){
Remove-ItemProperty -Path “REGISTRY::HKEY_USERS\$currentusersid\SOFTWARE\” -Name PasswordExpiration
}
New-ItemProperty -Path “REGISTRY::HKEY_USERS\$currentusersid\SOFTWARE\” -Name PasswordExpiration -Value $ExpDate
If (($TimeSpan.Days -le 10) -and ($TimeSpan.Days -ge -5)) {
Write-Output “Password Expires after $($TimeSpan.Days) days”
Exit 1
}
If ($logfilename) {
Stop-Transcript | Out-Null
}
Exit 0
and then the remediation:
$loggedonuser = ((Get-WMIObject -ClassName Win32_ComputerSystem).Username).Split(‘\’)[1]
# Gets the USID of the current logged on user
$currentusersid = Get-WmiObject -Class win32_computersystem | Select-Object -ExpandProperty Username | ForEach-Object { ([System.Security.Principal.NTAccount]$_).Translate([System.Security.Principal.SecurityIdentifier]).Value }
$ExpDate = Get-ItemPropertyValue -Path “REGISTRY::HKEY_USERS\$currentusersid\SOFTWARE\” -Name PasswordExpiration
The Message ist then like this:
$Base64EncodeString = “YOUR MESSAGE”
$Base64Text = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($Base64EncodeString))
$ExpText = $Base64Text +” ” + $ExpDate