Whenever an application is live it’s important to keep it up and running without downtime. But what if your application stops suddenly just because you forget to update the expired ClientSecrets? It is going to affect your customers and business and you will get lot of escalation emails. Being an application owner, you don't want your application to have Application Downtime.
To avoid all these let’s implement a PowerShell script with help of Azure Function to monitor the expiration of your Azure AD app client secrets and send an email to the respective users in advance before it expires.
I hope you are you are already aware of below technologies which is needed for this implementation, if not please have a look at each official documentation.
Create Azure Timer Trigger
- Login to Azure Portal
- Search for Azure Function App and create the resource with required details.
- Ensure to choose the Function runtime as PowerShell Core

- Once the resource got created, go to the Functions menu in the left pane and click “Create Trigger” then choose “Timer Trigger”

How to change the schedule in Timer Trigger
- Timer triggers are based on schedules, if you want to change the time when the trigger should happen.
- Click on your function name and in the upcoming screen go to “Code + Test” menu.
- Select function.json from the files dropdown and look for the “schedule” object in the json to change the schedule timing.
- Learn more about the which expressions to use for the timing, please have a look at NCRONTAB expressions

How to enable AZ PowerShell module in Azure Functions
Azure functions by default support AZ module to perform operations in Azure Resources. Below are the steps to enable it.
Have a look at Azure PowerShell module commands to learn more about it.
- Go to your Function App and in the left pane click App Files.
- Select “requirements.psd1” from the files dropdown and uncomment the Az module as the below screen shot. If you wanted specific Az module version also you can modify it.

Enabling Managed Identity in Azure Functions
We would need to enable Managed identity to grant permission to our Azure Function app to access the Active Directory. Please take a look at use managed identities for App Service and Azure Functions to enable it.
How to assign Application administrator permission to Azure Managed identity
- Go to Azure Active Directory and in the landing, page select “Roles and administrators”

- In the upcoming screen, select “Application administrator” and then click “Add Assignments”.

- Then search your managed identity and grant the permission. After granting permission you can find your Azure function app identity under assignments as below,

Now all the perquisites and the setup done for our implementation let’s see the actual script to do that,
PowerShell Code in Azure Function to monitor Azure AD Client Secrets
- Add the expiration days to monitor and Webhook post link in Function App configuration as below, so it can be accessed in the function code.

- Now our script will read all the AD apps for which client secrets will expire before the configured timeframe and fetch the meta details like Description, ExpiryDate, AppID, AppObjectID, AppName and Key Identifier.
- Even if there are more than one client secrets are expiring in the same Azure AD app. It will also be returned as a separate object in the resulting array.
- The last few lines of the code will post the Array object to WebHook.
- Here is the full code go to your Timer Trigger Function and click “Code + Test” and replace the code editor with below code
# Input bindings are passed in via param block. param($Timer) #secret expiration date filter (for example 30 days) $LimitExpirationDays = $env:LimitExpirationDays $SecretsToExpire =@() #Retrieving the list of secrets that expires in the above range of days Get-AzADApplication | ForEach-Object { $app = $_ @( Get-AzADAppCredential -ObjectId $_.Id ) | Where-Object { $_.EndDateTime -lt (Get-Date).AddDays($LimitExpirationDays) } | ForEach-Object { $expiringSecret = @{ AppName = $app.DisplayName AppObjectID = $app.Id AppApplicationId = $app.AppId SecretDisplayName = $_.DisplayName SecretKeyIdentifier = $_.KeyId SecretEndDate = $_.EndDateTime } $SecretsToExpire += $expiringSecret } } #Printing the list of secrets that are near to expire if($SecretsToExpire.Count -EQ 0) { Write-Output "No secrets found that will expire in this range" } else { Write-Output "Secrets that will expire in this range:" Write-Output $SecretsToExpire.Length Write-Output $SecretsToExpire | ConvertTo-Json $headers = @{ 'Content-Type' = 'application/json' } if($SecretsToExpire.Length -EQ 1){ Invoke-RestMethod -Method 'Post' -Headers $headers -Uri $env:LogicApp_API_Url -Body (ConvertTo-Json @($SecretsToExpire)) } else{ Invoke-RestMethod -Method 'Post' -Headers $headers -Uri $env:LogicApp_API_Url -Body ($SecretsToExpire | ConvertTo-Json) } }
Now the next step is to send notification to the user, it can be any kind of webhook/API URL which receive your POST call from PowerShell and process the Request data to send emails. You can make this process easier when you go with Azure Logic Apps as it has default connectors for Mailing.
Now our eyes are on the expiring Client Secrets before it expires which monitors it on daily basis.
Note: If you don’t want to receive the emails or run the trigger, go to your Timer Trigger -> Overview page and Disable the Function temporarily.

Greg GrovesPosted Feb 1, 2023, 2:58 PM
What should be the value of LogicApp_API_Url? Im getting a URI error and I think its because this is empty. 2023-02-01T15:30:36Z [Error] ERROR: Invalid URI: The hostname could not be parsed. Exception : Type : System.UriFormatException TargetSite : Name : CreateThis DeclaringType : uri MemberType : Method Module : System.Private.Uri.dll Message : Invalid URI: The hostname could not be parsed. Source : System.Private.Uri HResult : -2146233033 StackTrace : at System.Uri.CreateThis(String uri, Boolean dontEscape, UriKind uriKind, UriCreationOptions& creationOptions) at System.Uri..ctor(String uriString) at Microsoft.PowerShell.Commands.WebRequestPSCmdlet.CheckProtocol(Uri uri) at Microsoft.PowerShell.Commands.WebRequestPSCmdlet.PrepareUri(Uri uri) at Microsoft.PowerShell.Commands.WebRequestPSCmdlet.GetRequest(Uri uri) at Microsoft.PowerShell.Commands.WebRequestPSCmdlet.ProcessRecord() at System.Management.Automation.Cmdlet.DoProcessRecord() at System.Management.Automation.CommandProcessor.ProcessRecord() CategoryInfo : NotSpecified: (:) [Invoke-RestMethod], UriFormatException FullyQualifiedErrorId : System.UriFormatException,Microsoft.PowerShell.Commands.InvokeRestMethodCommand InvocationInfo : MyCommand : Invoke-RestMethod ScriptLineNumber : 47 OffsetInLine : 5 HistoryId : 1 ScriptName : C:\home\site\wwwroot\TimerTriggerEveryOtherDay\run.ps1 Line : Invoke-RestMethod -Method 'Post' -Headers $headers -Uri $env:LogicApp_API_Url -Body ($SecretsToExpire | ConvertTo-Json) PositionMessage : At C:\home\site\wwwroot\TimerTriggerEveryOtherDay\run.ps1:47 char:5 + Invoke-RestMethod -Method 'Post' -Headers $headers -Uri $env:Logi … + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ PSScriptRoot : C:\home\site\wwwroot\TimerTriggerEveryOtherDay PSCommandPath : C:\home\site\wwwroot\TimerTriggerEveryOtherDay\run.ps1 InvocationName : Invoke-RestMethod CommandOrigin : Internal ScriptStackTrace : at <ScriptBlock>, C:\home\site\wwwroot\TimerTriggerEveryOtherDay\run.ps1: line 47