Password Reminder with Proactive Remediation for AAD joined devices

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.

  1. Open Azure AD
  2. Navigate to App registrations
  3. Select New registration
  4. Select Accounts in this organizational directory only
  5. Select a fitting Name for your application, I chose “IntunePasswordNotification” but it doesn’t matter
  6. 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

Application ID and Tenant ID 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

  1. Select Add a permission
  2. Chose Microsoft Graph
  3. Chose Application permissions
  4. Search User.Read.All
  5. Mark the box for User.Read.All under User
  6. Add permissions
  7. Review that the correct permissions have been granted then Select Grant admin consent for “Tenant”
Don’t forget to Grant the Permissions

Now we just need to create a way for us to authenticate against the Application, navigate to the Certificates & secrets tab

  1. Make sure you have Client secrets highlighted
  2. Select New client secret
  3. 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
  4. Add
  5. Make note of the Value, Secret ID, Description & Expiration date
    1. This will be the only time the Value is visible, after leaving this tab the secret is gone forever
    2. 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.

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

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.

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.

  1. Open Endpoint Manager
  2. Navigate to Reports
  3. Navigate to Endpoint analytics
  4. Navigate to Proactive remediation’s
  5. 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

Remediation Script

This Post Has 51 Comments

  1. David

    Great script.
    Works great for Azure joined only but does not seem to work on Hybrid joined devices.
    Have you seem that by chance?

  2. David

    Do you know a way to also make this work for Hybrid joined devices?

    1. Viktor

      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

  3. NikTes

    Terrific post! I don’t have any prior domain knowledge, but this is obviously the best solution to the problem.

  4. David

    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

  5. David

    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

    1. Viktor

      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.

  6. Shaz

    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 👍🏽

  7. Shaz

    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 👍🏽

    1. Viktor

      Thanks man!

  8. George

    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

  9. George

    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

    1. Viktor

      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!

    2. Lile

      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

  10. Chris

    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.

    1. Viktor

      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

      1. JHW

        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?

        1. Viktor

          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

  11. Anonymous

    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.

    1. Tory0162

      Wanna share your solution?

  12. ADR

    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))

    1. Viktor

      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

  13. ADR

    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.

    1. Viktor

      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

  14. Ian

    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!

    1. Viktor

      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

  15. Craig

    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!

    1. Viktor

      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

  16. Jannie

    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?

    1. Viktor

      Hi, if it fails to gather the current azure ad user its probably an error with the enterprise application. Did you grant permissions?

  17. Kanobi

    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.

    1. Viktor

      Hi,
      Thanks!! Great to hear you enjoyed it and thanks for sharing your adjustments!

      Best regards, Viktor

  18. Carl M.

    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

    1. Viktor

      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

  19. David

    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

    1. Robinson

      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

      1. Lile

        Hi Robinson, facing the same error.
        Were you able to resolve this?

      2. Lile

        Hi Robinson,
        did you find a solution for this? I’m facing the same issue

      3. Lile

        Hi Robinson, did you find a solution for this? I’m facing the same issue

    2. Lile

      Hi David, facing the same error.
      Were you able to resolve this? Do you remember what was the cause?

  20. Anonymous

    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/

  21. Matt

    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?

  22. PK

    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

  23. Oleksii

    Hello Viktor,
    How can we update that script to block users from Sign In if the password was not changed and expired?

    Thank you.

    1. Sam

      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

  24. AET

    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.

  25. AET

    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.

  26. Anonymous

    This is no longer available in Endpoint Analytics and now requires an E3 or higher license. Has anyone come up with an alternate solution?

  27. Anonymous

    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?

    1. Anonymous

      Same user here. Disregard, I have found the solution.

      1. Lubos

        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

Leave a Reply