Catching expiring Entra app credentials before they cause an outage
The built-in Entra recommendation for expiring app credentials only looks 30 days out and lives in a portal tab nobody checks daily. Here is a Microsoft Graph PowerShell runbook that closes that gap.
I’ve lost count of how many “the integration just stopped working” tickets turned out to be an expired client secret on an app registration nobody had looked at since the person who created it left the company. The credential doesn’t fail loudly in advance — it just stops working the moment it expires, usually on a Monday, usually for something that was quietly load-bearing. Entra ID actually ships a built-in recommendation for this. It’s just not enough on its own, and it’s worth understanding exactly why before you build something better on top of it.
What the built-in recommendation actually covers
Microsoft Entra’s Recommendations feature includes applicationCredentialExpiry, which
flags app registration credentials — secrets and certificates — expiring within a fixed
30-day window. You’ll find it under Entra ID > Overview > Recommendations in the
admin center, or query it directly via Graph:
GET https://graph.microsoft.com/beta/directory/recommendations
GET https://graph.microsoft.com/beta/directory/recommendations/{tenantId}_ApplicationCredentialExpiry/impactedResources
There’s a companion recommendation for service principal credentials, covering the enterprise application side. Both are genuinely useful — but three limitations mean they can’t be your only line of defense:
- The 30-day window is fixed. You can’t widen it to give an application owner 90 days of lead time, or narrow it to surface only truly urgent cases.
- Some recommendations, including this one, require an Entra recommendation license
(
microsoftEntraWorkloadIdin the API response) — worth confirming against your own tenant’s licensing before you rely on it as your sole safety net. - It’s a portal tab and a Graph endpoint, not a notification. Nothing pushes it to the team that owns the app unless someone remembers to check, or you build the plumbing yourself.
That last point is the one worth solving with automation.
Reading credential expiry with Microsoft Graph PowerShell
Get-MgApplication doesn’t return PasswordCredentials or KeyCredentials unless you ask
for the specific application — a full listing gives you the app objects, but you need a
second call per app to get its credentials:
Connect-MgGraph -Scopes 'Application.Read.All'
$thresholdDays = 30
$now = Get-Date
$apps = Get-MgApplication -All
$expiring = foreach ($app in $apps) {
$creds = Get-MgApplication -ApplicationId $app.Id |
Select-Object -ExpandProperty PasswordCredentials, KeyCredentials -ErrorAction SilentlyContinue
$allCreds = @($app.PasswordCredentials | ForEach-Object { $_; $_ | Add-Member -NotePropertyName Type -NotePropertyValue 'Secret' -PassThru -Force }) +
@($app.KeyCredentials | ForEach-Object { $_; $_ | Add-Member -NotePropertyName Type -NotePropertyValue 'Certificate' -PassThru -Force })
foreach ($cred in $allCreds) {
$daysLeft = ($cred.EndDateTime - $now).Days
if ($daysLeft -le $thresholdDays) {
[PSCustomObject]@{
Application = $app.DisplayName
AppId = $app.AppId
CredType = $cred.Type
CredName = $cred.DisplayName
ExpiresOn = $cred.EndDateTime
DaysLeft = $daysLeft
}
}
}
}
Two things worth knowing before you copy this into a runbook:
- You can’t filter server-side on credential expiry —
$filterdoesn’t reach intopasswordCredentials/keyCredentials— so this always means pulling every application in the tenant and filtering client-side. On a large tenant, that’s the slow part; budget for it in your Automation runbook’s timeout. - Service principals (the enterprise application object, distinct from the app
registration) can carry their own credentials too, notably federated identity
credentials. Run the same pattern against
Get-MgServicePrincipal -Allif your tenant relies on those.
Wiring in ownership and an actual notification
The recommendation’s real gap isn’t detection, it’s routing. Get-MgApplicationOwner
gets you from an app to the person who should be paged:
foreach ($item in $expiring) {
$owners = Get-MgApplicationOwner -ApplicationId (
Get-MgApplication -Filter "appId eq '$($item.AppId)'"
).Id
$item | Add-Member -NotePropertyName OwnerUpn `
-NotePropertyValue ($owners.AdditionalProperties.userPrincipalName -join '; ')
}
From there, push it somewhere people actually look — a Teams channel via an incoming
webhook, an email through Microsoft Graph’s sendMail, or a row in a governance dashboard
if you’re already centralizing this kind of signal. The exact sink matters less than the
principle: the check has to run unattended, on a schedule, and land somewhere that
generates a ticket or a ping — not a portal tab someone has to remember to open.
An Azure Automation runbook with a managed identity (granted Application.Read.All
application permission via Graph, since this needs to run without a signed-in user) running
daily is enough for most tenants. Keep the threshold configurable as a runbook parameter —
90 days for a first warning, 14 for an escalation — rather than hardcoding Entra’s fixed
30-day window.
The takeaway
The built-in recommendation is a good backstop, not a monitoring strategy — a fixed 30-day window, a license dependency worth double-checking, and no notification path of its own. The Graph PowerShell pattern above costs maybe thirty minutes to wire into a scheduled Automation runbook, and it’s the difference between finding out about an expiring secret from a dashboard you check on your own schedule versus finding out from an incident ticket on a Monday morning.
Sources & further reading: PowerShell sample: export app registrations with expiring secrets and certificates, Recommendation to renew expiring application credentials, Recommendation to renew expiring service principal credentials, Get-MgApplication, Add and manage application credentials in Microsoft Entra ID.