Defusing the Time Bomb: Automating Entra ID App Secret Expiry Alerts
Welcome back to the terminal garden.
Every M365 admin knows this scenario: Itβs a quiet Friday afternoon. Suddenly, your helpdesk lights up. A critical HR synchronization script has stopped working, your automated backup pipeline is throwing unauthorized errors, or a third-party API integration just died. You spend an hour digging through logs only to discover the most mundane root cause possible: An Entra ID App Registration client secret quietly expired.
Entra ID enforces a maximum lifespan for client secrets and certificates, but it is notoriously bad at proactively warning the right people. Let's build a serverless early warning system using Azure Automation, Microsoft Graph, and a Teams Workflow.
The Serverless Setup & Costs
We do not want to run this script manually, and we definitely do not want to store a service account password inside the script. The most elegant solution is Azure Automation with a System Assigned Managed Identity.
π‘ Good to Know: What does this cost?
If you have never used Azure Automation before, don't worry about the bill. Microsoft includes 500 minutes of free job run time per month. This script takes about 15β30 seconds to execute. Even if you run it daily, you will consume less than 15 minutes a month. The Managed Identity and the Teams Workflow are also included in standard subscriptions. Total cost: 0.00 β¬ / month.
Step 1: Create the Automation Account & Grant Permissions
- In the Azure Portal, create an Automation Account, and ensure System assigned managed identity is checked.
- The Azure Portal lacks a GUI to grant Microsoft Graph permissions to this identity. Run this locally on your PC (requires Admin rights):
PowerShell
Connect-MgGraph -Scopes "AppRoleAssignment.ReadWrite.All", "Application.Read.All" $ManagedIdentityName = "YOUR-AUTOMATION-ACCOUNT-NAME"
$PermissionName = "Application.Read.All" $MSI = Get-MgServicePrincipal -Filter "displayName eq '$ManagedIdentityName'"
$Graph = Get-MgServicePrincipal -Filter "appId eq '00000003-0000-0000-c000-000000000000'" $Role = $Graph.AppRoles | Where-Object Value -eq $PermissionName
New-MgServicePrincipalAppRoleAssignment -ServicePrincipalId $MSI.Id -PrincipalId $MSI.Id -ResourceId $Graph.Id -AppRoleId $Role.Id
Step 2: Import the Modules (The Dependency Trap)
Do not just import Microsoft.Graph.Applications! The Connect-MgGraph cmdlet and core authentication layers live in the base module. If you skip it, your runbook will crash with a missing cmdlet error.
- In your Automation Account, go to Modules -> Add a module.
- Import
Microsoft.Graph.Authenticationfirst. - Import
Microsoft.Graph.Applicationssecond.
The Prerequisites: Setting up the Teams Webhook
Microsoft retired the classic "Incoming Webhooks" in 2024. The modern way is via Teams Workflows:
- Open Teams, go to your IT channel, click ... and select Workflows.
- Search for the template: Post to a channel when a webhook request is received.
- Name it, select your channel, click Add workflow, and copy the generated URL.
The Runbook Code
Create a new PowerShell Runbook in your Automation Account, paste this code, and link it to a weekly schedule.
PowerShell
# =====================================================================
# maik.ing | the terminal garden - App Secret Expiry Alert
# =====================================================================
Import-Module Microsoft.Graph.Authentication
Import-Module Microsoft.Graph.Applications # 1. Configuration
$TeamsWebhookUrl = "YOUR_TEAMS_WORKFLOW_URL_HERE"
$DaysThreshold = 30
$CurrentDate = Get-Date # 2. Connect via Managed Identity
Connect-MgGraph -Identity
$Apps = Get-MgApplication -All
$ExpiringItems = @() # 3. Check Secrets and Certificates
foreach ($App in $Apps) {
# Check Client Secrets
foreach ($Secret in $App.PasswordCredentials) {
if ($null -ne $Secret.EndDateTime) {
$DaysLeft = ($Secret.EndDateTime - $CurrentDate).Days
if ($DaysLeft -ge 0 -and $DaysLeft -le $DaysThreshold) {
$ExpiringItems += [PSCustomObject]@{
AppName = $App.DisplayName; AppId = $App.AppId
Type = "Client Secret"; DaysLeft = $DaysLeft
ExpiryDate = $Secret.EndDateTime.ToString("yyyy-MM-dd")
}
}
}
}
# Check Certificates
foreach ($Cert in $App.KeyCredentials) {
if ($null -ne $Cert.EndDateTime) {
$DaysLeft = ($Cert.EndDateTime - $CurrentDate).Days
if ($DaysLeft -ge 0 -and $DaysLeft -le $DaysThreshold) {
$ExpiringItems += [PSCustomObject]@{
AppName = $App.DisplayName; AppId = $App.AppId
Type = "Certificate"; DaysLeft = $DaysLeft
ExpiryDate = $Cert.EndDateTime.ToString("yyyy-MM-dd")
}
}
}
}
} # 4. Alerting via Teams Adaptive Card
if ($ExpiringItems.Count -gt 0) {
$Message = ""
foreach ($Item in $ExpiringItems) {
# Deep Link directly into the Entra Admin Center
$AppUrl = "https://entra.microsoft.com/#view/Microsoft_AAD_RegisteredApps/ApplicationMenuBlade/~/Credentials/appId/$($Item.AppId)/isMSAApp~/false"
$Message += "- **[$($Item.AppName)]($AppUrl)** ($($Item.Type)) expires in **$($Item.DaysLeft) days** ($($Item.ExpiryDate))`n"
} $Payload = @{
type = "AdaptiveCard"
'$schema' = "http://adaptivecards.io/schemas/adaptive-card.json"
version = "1.4"
body = @(
@{ type = "TextBlock"; size = "Large"; weight = "Bolder"; color = "Attention"; text = "π¨ Entra ID App Expiry Warning" },
@{ type = "TextBlock"; text = $Message; wrap = $true }
)
} | ConvertTo-Json -Depth 10 # UTF-8 Encoding Fix: Prevents Emojis from turning into "??" in Azure Automation
$Utf8Payload = [System.Text.Encoding]::UTF8.GetBytes($Payload)
Invoke-RestMethod -Uri $TeamsWebhookUrl -Method Post -Body $Utf8Payload -ContentType "application/json; charset=utf-8"
} Disconnect-MgGraph
The Nordic Breeze Takeaway
Two important lessons learned from building this scribble:
-
Module dependencies matter in Azure: Sub-modules like
Microsoft.Graph.Applicationswill silently fail or break cmdlets ifMicrosoft.Graph.Authenticationis not imported alongside them. -
Mind your payload encoding: When running in cloud sandbox runbooks, always convert your JSON payload explicitly to UTF-8 bytes before sending it to an endpoint. Otherwise, your nice alert emojis will turn into question marks (
??).
Set it up once, link it to a weekly schedule, and enjoy quiet Friday afternoons.
β Support the Terminal Garden
If this scribble saved you a few hours of reading Microsoft documentation, consider fueling the next late-night PowerShell session.
BTC: bc1qp06uajaxpu2dxzqhqsvcxesp7uydgy5829nag2
ETH: 0x54644d3af213fed1cc6e2c96d2dcd2189014b6f9
SOL: Gwu9BKKYe97ukrVUYDQ3Mp96Ru3hu9HK82oe4Lg4haBp
Building, breaking, and automating runs on coffee and community. Thank you!