maik.ing | the terminal garden
about 3 hours ago

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

  1. In the Azure Portal, create an Automation Account, and ensure System assigned managed identity is checked.
  2. 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.Authentication first.
  • Import Microsoft.Graph.Applications second.

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:

  1. Module dependencies matter in Azure: Sub-modules like Microsoft.Graph.Applications will silently fail or break cmdlets if Microsoft.Graph.Authentication is not imported alongside them.
  2. 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!