Windows Toast Notification Based Password Expiration Reminders

Unfortunately, even as of February 2023, Microsoft still doesn’t have a good solution (or really any solution) to provide employees on Azure joined devices with notifications about the fact that their password will soon expire. This is an unpleasant surprise for anyone who is used to their old local domain joined devices and the simple notifications they used to get from the task tray.

While there isn’t a perfect solution to this as your Azure account doesn’t have a magic “My password will expire on X/X/X” property, there is a way to achieve something similar with PowerShell and Toast notifications.



A brief tangent about proper credit and reading carefully.


The original code this work is based on is by Viktor over here: Password Reminder with Proactive Remediation for AAD joined devices – Something went right (smthwentright.com)

I found this in a hurry to solve a problem because IT is always on fire, skimmed the article, ran the code and saw the popup, and said to myself “Oh cool, I can tear this apart, combine it with similar code to what our Log Analytics Function App uses, and create something even better!”

Well, I did just that. It works great. So, I started writing this blog. I knew I needed to credit Viktors work so I found my way back to the article… and I promptly noticed the plain text URL at the top of his blog which goes to a second article about securing it with a function app. My Function app was based on the Function App from the Intune Enhanced Inventory project by Jan Ketil Skanke of the MsEndpointMgr team which I am intimately familiar with due to my Log Analytics work. Well guess what, so is Viktors. That’s very awkward as I had thoroughly re-invented (or more so re-tweaked) the wheel. But I like my wheels tread more, so I am writing this anyways.

What’s different about this guide?

Unlike the other guide, you won’t need to create or use…

  • An app registration with permissions
  • An App Secret that expires every so often
  • A key vault for securely providing the secret to your Function app.

Among other fun improvements:

  • This also works for hybrid joined devices so you can have some harmony between environments.
  • This includes some automatic registry key flipping to ensure your employees can’t just hide the notifications which is easy to do via the tripple dots right on the notification.
  • Notifications won’t show sensitive content on the lock screen.
  • I also altered the app the notifications come from the “Work or School Account” rather than the Company Portal although this can be changed back easily.
  • This only uses one script, a detections script. This way, we only call to our function once and our popup can contain information about how many days out the expiration is!

Link to the GitHub where all code can be found:

GitHub Project

What do you need?

  • A Detection Script to run on devices, call our app, and display notifications.
  • A Resource Group to hold everything we are about to put in Azure. You likely already have one.
  • A Storage account to hold our Blob image. You likely already have one.
  • An Azure Function app
  • Licensing for Proactive Remediations*

*For the sake of just testing, you can just run the script manually out of Visual Studio for instance. All Proactive Remediations does is automate execution.

How does it work?

It’s actually pretty simple.

We will deploy a PowerShell script to devices on a regular (changeable) basis via Intune Proactive Remediations. If you have never used Proactive Remediations, it’s basically the equivalent of Task Scheduler inside Intune. That is if Task Scheduler could only run PowerShell, didn’t like to run at the exact times you tell it, and took 5 hours for your device to detect the assignment or a change in the script.

This script will gather the username of the user on the machine, the devices tenant ID, devices Azure ID, and compile them into a JSON. This JSON is sent to “authenticate” to our Function App and let it know what user to query.

Update: Additionally, our script will call to the Function App with a URL + Key to further protect calls to the Function. The key only serves as a way to call the Function.

Our Function App will use that information to validate it’s a request from the right Tenant and a real/enabled Azure Device ID in that tenant to “authentication” to the function. It then replies back with a JSON containing the provided accounts name and when the password was last set.

The script then validates the right account came back in reply, does some simple math to figure out when their password will expire (assuming you have a consistent policy across the board) and throw a notification accordingly.

Again, there is no way to actually tell when the password will truly expire, we can only do math given the last time it was set.

The Toast notification behavior is a bit odd. They seem to stick around for hours although sometimes randomly dismiss, I believe because too many total notifications have stacked up. Those that appear on the lock screen will vanish the moment you interact with the machine, and may hide inside the notification tray or present upon logging in.

Warning:

Regarding the “Authentication” – all someone would need in order to make bad-faith queries to this Function app is the Tenant ID of your tenant and the Azure ID of a real device in your tenant. That’s something that can be gathered off any device by a standard user. What a bad actor could get in return (At the least) is the date a password was last set on any given account in the tenant. As always, you should refer to the disclaimer at the bottom regarding the use of this code and guide.

If you really wanted to, and I may update it to do this, you could code in logic to not return information for account names that match certain criteria or even accounts in certain AD groups.

Additionally, this may not function well or at all on a multi-session box as the script will see more than one owner of explorer.exe.

Update: The script now calls to the Function using Function level security. Basically, the URL now has a key attached to it. This key is in plain text within the script however, all it provides is access to talk to the Function. I can’t think of any reason to not use it as simply another helpful layer. Now someone would need to obtain this key in addition to the above items.

Azure Resource Group, Storage Account, and Blob Container Image:

This Script makes use of calling an image file you can use to better brand the popup. This image is stored inside a public Blob Container in Azure. If you aren’t sure how to make that, you can check out my blog here: Using Azure Blob Storage to Host Media for Intune Wallpaper Policy and PowerShell – Getting the Most Out of Azure (azuretothemax.net)

Azure Function App Creation:

This will all make a lot more sense if we start with the Function App. You can either do this very manually if you want to learn, or just click the below to use a template to create a generic Function much faster. This function will not be fully setup internally but will make things easier. This template is in the GitHub if you have questions about what it creates and how it works. It’s based off the one MSEndPoingMGR used for their Function App.


Using the Template:

This template effectively does the same thing that manually creating it will do. However, it creates it much more easily and it gives things like your storage account and App Plan a much more friendly name that’s based on the name of the Function App. Supply the template with the subscription to place the resources in.

Click here to get started:

You will need to provide a Subscription to place the Function App in, a Resource Group (must be entered twice exactly the same), a Region, a Service Plan (Y1 is consumption), and any appropriate tags.

You must also provide the Function a name which is a bit tricky. As the information bubble will explain, the name must be globally unique. No other tenant can have already taken the name. Unfortunately, just because you get a green check on this box, it doesn’t mean the name is available and the deployment may fail because of that. If this happens, you may need to cleanup any resources it already created such as the Storage Account, App Service Plan, or Applicaiton Insights.



If successful, you should see something like this indicating several objects have been made with naming that all starts with or matches your provided Function App Name.



Creating the Function manually:

Know that this does have the disadvantage of some less clean names for your App Service Plan, Storage Account, and Application Insights.

  1. Head to the Azure Home.
  2. Choose Create a Resource.
  3. Search for Function App and locate the Microsoft offering. Hit Create.
  4. Place it in your Resource Group.
  5. Provide the Function a Name.
  6. Chose Code, PowerShell Core, 7.2 (or newer), place it in the same region as your storage account, leave the OS on Windows, and choose the plan that best suits your needs. I will be using Consumption.
  7. I recommend letting the Function App have it’s own Storage Account. (Create New)
  8. Networking: You do need public access on for this to work. This is because we will call this function from outside Azure networks.
  9. Monitoring: I would highly recommend you Enable application insights. Without this you will not have many options for troubleshooting the Function App. This does charge you when on, and it can be much more expensive than the function itself, but there is an easy way to turn it off and on as needed which I will cover later.

    Update: Saving a Buck by Temporarily Disabling Application Insights – Getting the Most Out of Azure (azuretothemax.net)
  10. Definitely do NOT connect it to my GitHub URL.
  11. And don’t forget to actually hit Create



Azure Function App Setup:

We now need to deploy code to the Function App.

  1. Head into your Function app and find Configuration under Settings on the left hand side.
  2. Under Application Settings choose New Application Setting
  3. Name it TenantID and give it the value of your Tenant (can be found in Azure AD home menu)
  4. Hit Save and confirm you want to reboot the app.

    You should have something like this in the end.
  1. Then, find Functions on the left hand side under the Functions header
  2. Choose Create
  3. Choose Develop in portal and the HTTP Trigger template (may take a moment to load)
  4. Give the function a name
  5. Choose Function* – SEE THE NOTE!
  6. You should be inside the function once it creates, change to the Code + Test menu.
  7. By default there will be a template PowerShell script here, blow it away and replace it with the contents of FunctionApp.Ps1
  8. Hit Save
  9. Then go back to the Overview menu of the Function and near the top (next to delete) click Get Function URL. Get the Default Function Key version of the URL and store it somewhere in the meantime. This is needed later for our script.

*Originally this was Anonymous which was dictation that came from the MsEndpointMgr team. However, after playing with it on my end, I don’t see any reason not to use Function level authentication. Read more here. The only thing this changes is that when you get the URL for the Function, choose Default (Function Key). This will provide you a URL with a key on it, and that key lets you call the function. Without that key you can’t call the Function. I wouldn’t consider this sure-fire security, but I can’t think of a reason not to use it either. As always, see the disclaimer.


Enabling Managed Identity:

This step is the key to the next section. If you used the template, it’s already on. If not, this is how you turn it on. This allows permissions to be assigned to the Function App itself.

  1. Head into your Function App
  2. Find Identity under Settings on the left hand side
  3. Change the status to On
  4. Hit Save and Yes



Granting your Function App Permissions:

Now we need to grant the function app itself permissions. Rather than using an app registration, we can simply give the function app permissions directly to do what it needs to.

Thanks again to the msendpointmgr team for this one!

If you did not use the template, you first need to enable a managed identity for your app!

Find the FunctionAppPermissionScript on GitHub.

  1. If you don’t have the Graph modules installed, you need to run line two just one time. This could take a while.
  2. There are two variables you need to supply information to near the top. The TenantID is just that and can be found near the top of the Azure AD home screen. The ServicePrincipalAppDisplayName is just the name of your function app like “BobsFunctionApp”
  3. Execute the script and you will liekly be prompted to authenticate via a web browser.
  4. Once complete, it will grant your Function app the Device.Read.All Permission. This is to query graph and make sure your provided Azure Device ID is real and enabled.
  5. Then, comment out line 15, uncomment 16, and re-run the script in order to also grant Users.Read.All. This is to grant the ability to read information about users from Graph, including when their password was last set.
  6. Then, go into Azure Activve Directory, Enterprise Applications, and change the Application Type to All Applications
  7. You should be able to find your Function App in here by name (type it exactly).
  8. Once inside the app, look for Permissions on the left hand side
  9. You should see your two permissions have been added although, you may need to Grant Admin Consent at the top to fully apply them. I did not, but could have sworn I have in the past.


Setting up the Detection Script:

Open up your detection script and look near the top. Here is where we store most of the customizable variables.

  • FunctionURL: This is where you paste the URL you just got at the tail end of the last section. Again, see the note pertaining to using Function-level Authorization instead of Anonymous.
  • PasswordExpirationDays: How long it takes for passwords to expire
  • MinimumDays: T-minus how many days out to start showing the popup. Turn it higher for easy testing.
  • HeroImageFile: The full URL to your branding Image file
  • HeroImageName: Just the image file name
  • Action: Where the reset button takes you. By default we send you to the SSPR (Single Sign on Password Reset) page. This requires multiple Microsoft MFA factors. Adjust it to whatever portal you prefer.
  • logfilename: The name of your logfile to create and use
  • logfile: Controls the path to this file. If changed, make sure to alter the storage directory creation and hiding portion just under this.
  • You will find further customization regarding how the comparison math is done under the comment “#Further Math Controls”, such as the ability to stop showing it after a certain number of negative days.
  • You will find further customization regarding the message text under the commend “#Message Text”. This needs to be done after we know the expiration date.
  • Under the comment “#execution randomization” you will find the ability to randomize the scripts execution time. This is off by default but should be considered as it can affect billing. See the MsEndpointMGR article explaining this. That said, this obviously interrupts the script and delays when it could popup even more than Proactive Remediations itself does. Employees could accidentally interrupt the popup with a reboot/logoff and not see it that day. Weigh the cost and reward and understand the proactive remediation schedule you pick will affect this.
  • You may need to modify line under the comment “#Azure/local domain suffix!” with your specific local domain prefix instead of AzureAD.
  • You will NEED to modify the line under the comment “#Set Domain Suffix” with your domain suffix.
  • Swap around the commented lines under the comment “#App Control” to control which app notifications come from. I have had limited success with the Device Management app but it looks better if you can get it to work.

You should be able to run the script manually at this point and get valid information back from the function app pertaining to your accounts last password set. If you want to test the popup itself but are out of range, just adjust the settings at the top to something higher.

Do not proceed until you can run the script locally!

You should see something like… Obviously swap in your own wording and branding.

And the code outputs should look something like…

Although, this can also have warnings and notifications regarding the notification status of the app you chose.



Deploying the Detection Script:

Again, for testing, you might want to turn it to a really high warning period so you can see the popups.

  1. Head to Intune/MEM
  2. On the left hand side choose Reports
  3. Choose Endpoint Analytics
  4. Choose Proactive Remediations (No I don’t know why it’s so deep either)
  5. Choose Create Script Package
  6. Give it a Name and Description
  7. For the Detection Script use your script.
    Leave the remediation blank.
  8. Change both the “Run as User” and “64-bit” options to Yes
  9. Assign the remediation to a user group (start with a test group) on the schedule you desire.*
  10. If needed, another user group can be used for exclusions

Hurry up and wait. The deployment of proactive remediations takes HOURS. Reboot and sync to speed it up, but it will likely be 3-6 hours.

Eventually, you should start to get popups!

*Regarding the schedule, there are a few things to know for billing.

Keep in mind that Azure Function App billing cost is based on total calls and how many come at once. So, how often you run it and if you tell it to run every at the same time could be a problem.

That said, if you tell it to go daily at a device local time, odds are it won’t run right at X AM so there is a minor amount of spread due to that. Additionally, how many devices are in what time zone will obviously then affect this. Furthermore, devices like W365 and AWS swap to UTC when someone is logged on but disconnected. So those devices will throw the prompt at X AM UTC.

They do run whenever next possible if missed, so it’s possible employees on Monday will receive or have multiple popups waiting for them.

Saving Some Money:

If you want to save some money on this solution, check out my article here on how to control and enable Application Insights only when you need it.

Saving a Buck by Temporarily Disabling Application Insights – Getting the Most Out of Azure (azuretothemax.net)



Conclusion:

Once you have thoroughly tested, you can setup final scoping to a user group with an exclusion user group if need be.

Users will then start to see popups to help remind them to update their passwords in accordance with your policy and settings.

Again, keep in mind how often you run the script and thus call the app (and how many devices hit it at once) affect your billing.

There is almost certainly room for improvement on both the function and local script, and I can’t wait to see what folks think and/or who has ideas to improve it.


Bugs:

There is a bug with this script and Windows 11 pertaining to its inability to force the notification back on. See more here: Bug: Windows 11 and the Inability to Script Enabling Notifications for Apps – Getting the Most Out of Azure (azuretothemax.net)


Updates:

  • No longer requires an App Service Plan.
  • Now uses Function Authentication for better security.
  • Now with templated deployment of Function App


Update 8/2/2023: What is the future of this project?

I have curiously watched this article rack up decent views these past few months which I find a little surprising. But it’s made me think about what future this project does or does not have. Unfortunately, after a good amount of thinking, I don’t think it has much of one.

What could be done:

The only improvement I know could actually be made to this project would be to upgrade the client authentication to the latest HTTP(s) function app auth covered here: Security Update for HTTP(s) Function Apps – Getting the Most Out of Azure (azuretothemax.net)

There is no reason someone skilled in PowerShell couldn’t make this happen themselves though, the code for that new auth is on the GitHub (in that article, not this one). You can look at my Log Analytics learning series for plenty of information on how to produce something that uses this auth.

Why it won’t be:

This really comes down to three reasons.

  1. The bug in Windows 11 (see the Bugs section of this article just above)
    Obviously, the users on WIn11 having the ability to simply silence the notifications indefinitely is a problem. Unfortunately, it’s on Microsoft to fix and unlikely it ever will be. This isn’t a big nail in the coffin, but it is a sore point.

  2. It’s just not that good of a solution.
    Okay maybe that’s a bit harsh, but it’s certainly not up to my standards of accuracy by any means. Unfortunately, as I explained several times in this article, we have no way of actually knowing when a user’s password will expire. This is because the only value stored on an employee’s account is when they last set their password. There is no value/property/etc regarding when it will expire. That is a dynamically calculated event done by looking at several factors including but not limited to, how the account is configured (password set to never expire), what password policies exist, what the configuration of those policies are, how they are/aren’t assigned to a given user, and how they ultimately mix together to determine and action or lack thereof. There is simply no reasonable method to query and determine all that same information.

    At the end of the day, it would take action on Microsoft’s part such as adding a “last known expiration date” field to make this possible. Unfortunately, as I will explain below, I can already tell you this won’t happen because Microsoft has no interest in further supporting this area.

  3. Because it has no future.
    This may shock some of you, but Microsoft does NOT recommend the use of expiring passwords anymore. That means the problem this solution stands to solve has a ticking clock until it’s simply phased out of existence.

    Password expiration requirements do more harm than good, because these requirements make users select predictable passwords, composed of sequential words and numbers that are closely related to each other. In these cases, the next password can be predicted based on the previous password. Password expiration requirements offer no containment benefits because cybercriminals almost always use credentials as soon as they compromise them.”

    Source: https://learn.microsoft.com/en-us/microsoft-365/admin/misc/password-policy-recommendations?view=o365-worldwide#password-expiration-requirements-for-users

Given the above, it just doesn’t make sense to invest more time into something that won’t ever provide a solid data point, that Microsoft is both recommending against and actively fighting hard to get away from, and that is hindered by bugs they have little to no interest in fixing.

Disclaimer:

The following is the disclaimer that applies to all scripts, functions, one-liners, setup examples, documentation, etc. This disclaimer supersedes any disclaimer included in any script, function, one-liner, article, post, etc.

You running this script/function or following the setup example(s) means you will not blame the author(s) if this breaks your stuff. This script/function/setup-example is provided AS IS without warranty of any kind. Author(s) disclaim 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 author(s) be held 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 script or documentation. Neither this script/function/example/documentation, nor any part of it other than those parts that are explicitly copied from others, may be republished without author(s) express written permission. Author(s) retain the right to alter this disclaimer at any time. 

It is entirely up to you and/or your business to understand and evaluate the full direct and indirect consequences of using one of these examples or following this documentation.

The latest version of this disclaimer can be found at: https://azuretothemax.net/disclaimer/


Leave a comment