In the fast-paced world of enterprise IT, even small oversights can lead to major disruptions. One of those easily overlooked — yet critically important — tasks is monitoring Microsoft Entra (formerly Azure AD) application secret expirations.
Imagine this: everything is running smoothly in production until — bam! — an app suddenly fails because its secret expired. No alerts, no heads-up, just user complaints and a flurry of incident reports. Sound familiar?
Why Monitoring Application Secrets Matters
At its core, application secrets are credentials that apps use to authenticate themselves with Microsoft Entra ID. But unlike passwords, secrets come with an expiration date. If they aren’t renewed in time, the app’s authentication fails — and with it, the business processes it supports.
For organizations running dozens (or hundreds) of app registrations, staying ahead of these expirations is not optional — it’s essential. Secrets that silently expire can grind systems to a halt, disrupt integrations, and worst of all, trigger security incidents or SLA breaches.
A Common Problem in the Real World
When I first set out to automate monitoring for app secret expirations, I assumed it’d be simple. A few PowerShell lines here, an API call there, maybe some Logic App magic… problem solved, right?
Well, not quite.
Most of the tutorials and blog posts I found focused solely on fetching the expiration date of secrets — they showed how to query the data, but not how to operationalize it. I wanted something that would proactively notify the right people before things went south.
Eventually, I came across a helpful post on Microsoft Tech Community:
Use Azure Logic Apps to Notify of Pending AAD Application Client Secrets and Certificate Expirations
It was a solid foundation. The Logic App would periodically check secrets and send an email notification if one was nearing expiration.
But then… I hit a snag.
If an application had multiple owners — which, in the enterprise world, is very common — the Logic App would only notify the first listed owner. Everyone else? Left in the dark.
Not ideal. Especially when that one person is on PTO, has left the company, or, let’s be honest, just ignores emails from IT.
So, I decided to roll up my sleeves and build a PowerShell-based solution that:
- Queries all app registrations
- Checks for secrets or certificates nearing expiration
- Looks up all owners (not just the first one)
- Sends clear, actionable email notifications to each owner
Why Automate This? Let’s Talk Benefits
Here’s why every enterprise IT team should care about automating secret expiration alerts:
- Proactive Security – Timely notifications help you spot secrets that are about to expire — before they become a security risk or business disruption. It’s the difference between being reactive and being prepared.
- Reduced Downtime – Missed secret expirations lead to failed authentications, which means broken apps. Proactive alerts buy you time to renew secrets and avoid outages.
- No More Manual Tracking – Maintaining a spreadsheet of app secrets? Been there. Done that. Automation means less grunt work and fewer mistakes.
- Smart Notifications – By targeting all app owners — not just the first in line — you’re covering your bases. Even if someone’s on vacation, someone else sees the alert and can take action.
What This Script Does
- Authenticates to Microsoft Graph with the required permissions
- Queries all Entra app registrations
- Identifies app secrets and certificates that are expiring within a defined threshold (e.g., 30 days)
- Pulls all assigned owners for each app
- Sends an email notification to each owner with details about the impending expiration
- Sends an email notification to an administrator email, a distribution group or a shared mailbox containing a list of all secrets that are expiring
Use task scheduler to run this script from your on-premise environment. You can securely store the credentials to be used in the PowerShell script using the SecretManagement module. I’ve covered this in detail in an earlier post here. The ideal and preferred method is to use Azure automation account which is much more easier and secure, which I will cover in a future post.
# Connect to Microsoft Graph
Connect-MgGraph -Scopes "Application.Read.All","User.Read.All","AppRoleAssignment.Read.All"
# Set default value for the number of days until expiration
$DaysUntilExpiration = 30
# Email configuration
$SmtpServer = "smtp.yourdomain.com"
$From = "alerts@yourdomain.com"
$DefaultEmail = "entra_app_cert_notif@yourdomain.com"
# Function to send email
function Send-ExpirationAlert {
param (
[string]$To,
[string]$FirstName,
[string]$AppName,
[string]$SecretOrCertName,
[string]$SecretOrCertId,
[datetime]$EndDate
)
$Subject = "Alert: Secret or Certificate Expiration Notice for $AppName"
$Body = @"
<html>
<body>
<p>Hello $FirstName,</p>
<p>This is a notification that the secret or certificate named '$SecretOrCertName' for the application '$AppName' will expire on $($EndDate.ToShortDateString()).</p>
<p>Please contact Progressrail's Entra (previously known as Azure AD) Administrators and take the necessary actions to renew or replace the secret or certificate before it expires.</p>
<p>Details to provide the administrators:</p>
<ul>
<li>Application Name: $AppName</li>
<li>Secret or Certificate Name: $SecretOrCertName</li>
<li>Secret or Certificate ID: $SecretOrCertId</li>
<li>Expiration Date: $($EndDate.ToShortDateString())</li>
</ul>
<p><span style="color: red;">Please do not reply to this email, this mailbox is not monitored.</span></p>
<p>Thank you,<br>Your IT Team</p>
</body>
</html>
"@
Send-MailMessage -SmtpServer $SmtpServer -From $From -To $To -Subject $Subject -Body $Body -BodyAsHtml
}
# Function to send summary email
function Send-SummaryEmail {
param (
[string]$To,
[string]$Body
)
$Subject = "Summary: Secrets and Certificates Expiring in the Next 30 Days"
Send-MailMessage -SmtpServer $SmtpServer -From $From -To $To -Subject $Subject -Body $Body -BodyAsHtml
}
# Get the current date
$Now = Get-Date
# Query all applications
$Applications = Get-MgApplication -All
# Initialize a variable to store the summary of expiring secrets and certificates
$SummaryBody = @"
<html>
<body>
<p>Hello,</p>
<p>The following secrets and certificates are expiring in the next 30 days:</p>
<table border="1">
<tr>
<th>Application Name</th>
<th>Secret or Certificate Name</th>
<th>Secret or Certificate ID</th>
<th>Expiration Date</th>
</tr>
"@
# Process each application
foreach ($App in $Applications) {
$AppName = $App.DisplayName
$AppID = $App.Id
$ApplID = $App.AppId
$AppCreds = Get-MgApplication -ApplicationId $AppID
$Secrets = $AppCreds.PasswordCredentials
$Certs = $AppCreds.KeyCredentials
foreach ($Secret in $Secrets) {
$StartDate = $Secret.StartDateTime
$EndDate = $Secret.EndDateTime
$SecretName = $Secret.DisplayName
$SecretId = $Secret.KeyId
$Owners = Get-MgApplicationOwner -ApplicationId $App.Id
if ($Owners.Count -eq 0) {
# No owner information, send to default email
$FirstName = "Admin"
Send-ExpirationAlert -To $DefaultEmail -FirstName $FirstName -AppName $AppName -SecretOrCertName $SecretName -SecretOrCertId $SecretId -EndDate $EndDate
} else {
foreach ($Owner in $Owners) {
$Username = $Owner.AdditionalProperties.userPrincipalName
$OwnerID = $Owner.Id
if ($null -eq $Username) {
$Username = $Owner.AdditionalProperties.displayName
if ($null -eq $Username) {
$Username = '**<This is an Application>**'
}
}
# Extract first name from givenName or user principal name
$FirstName = $Owner.AdditionalProperties.givenName
if ($null -eq $FirstName -or $FirstName -eq '') {
$FirstName = $Username.Split('@')[0].Split('.')[0]
}
$RemainingDaysCount = ($EndDate - $Now).Days
if ($RemainingDaysCount -le $DaysUntilExpiration -and $RemainingDaysCount -ge 0) {
if ($Username -ne '<<No Owner>>') {
Send-ExpirationAlert -To $Username -FirstName $FirstName -AppName $AppName -SecretOrCertName $SecretName -SecretOrCertId $SecretId -EndDate $EndDate
}
}
}
}
# Add to summary if expiring in the next 30 days
if ($RemainingDaysCount -le $DaysUntilExpiration -and $RemainingDaysCount -ge 0) {
$SummaryBody += @"
<tr>
<td>$AppName</td>
<td>$SecretName</td>
<td>$SecretId</td>
<td>$($EndDate.ToShortDateString())</td>
</tr>
"@
}
}
foreach ($Cert in $Certs) {
$StartDate = $Cert.StartDateTime
$EndDate = $Cert.EndDateTime
$CertName = $Cert.DisplayName
$CertId = $Cert.KeyId
$Owners = Get-MgApplicationOwner -ApplicationId $App.Id
if ($Owners.Count -eq 0) {
# No owner information, send to default email
$FirstName = "Admin"
Send-ExpirationAlert -To $DefaultEmail -FirstName $FirstName -AppName $AppName -SecretOrCertName $CertName -SecretOrCertId $CertId -EndDate $EndDate
} else {
foreach ($Owner in $Owners) {
$Username = $Owner.AdditionalProperties.userPrincipalName
$OwnerID = $Owner.Id
if ($null -eq $Username) {
$Username = $Owner.AdditionalProperties.displayName
if ($null -eq $Username) {
$Username = '**<This is an Application>**'
}
}
# Extract first name from givenName or user principal name
$FirstName = $Owner.AdditionalProperties.givenName
if ($null -eq $FirstName -or $FirstName -eq '') {
$FirstName = $Username.Split('@')[0].Split('.')[0]
}
$RemainingDaysCount = ($EndDate - $Now).Days
if ($RemainingDaysCount -le $DaysUntilExpiration -and $RemainingDaysCount -ge 0) {
if ($Username -ne '<<No Owner>>') {
Send-ExpirationAlert -To $Username -FirstName $FirstName -AppName $AppName -SecretOrCertName $CertName -SecretOrCertId $CertId -EndDate $EndDate
}
}
}
}
# Add to summary if expiring in the next 30 days
if ($RemainingDaysCount -le $DaysUntilExpiration -and $RemainingDaysCount -ge 0) {
$SummaryBody += @"
<tr>
<td>$AppName</td>
<td>$CertName</td>
<td>$CertId</td>
<td>$($EndDate.ToShortDateString())</td>
</tr>
"@
}
}
}
# Close the HTML table and body
$SummaryBody += @"
</table>
<p>Thank you,<br>Your IT Team</p>
</body>
</html>
"@
# Send the summary email
Send-SummaryEmail -To $DefaultEmail -Body $SummaryBody
Monitoring Entra application secret expirations may not be the flashiest part of your security strategy, but it’s one of the most crucial. It’s also one of those tasks that’s easy to automate, but costly to ignore.
If you’re currently relying on manual processes — or using a Logic App that only pings one owner — consider leveling up your approach. A bit of PowerShell and planning can save you hours of downtime, reduce late-night incident calls, and help keep your environment secure.
Thank you for stopping by. ✌️