Find expiring Entra app secrets and certificates with PowerShell
A connector I had set up stopped bringing in new content. No error anyone noticed, it just went quiet. When I opened the app registration in Entra, the client secret had expired three months earlier. Nothing warned me. It stopped authenticating on the expiry date and kept failing quietly after that.
This is the trouble with app secrets and certificates. Every one of them has an expiry date, Entra does not chase you about it, and the day it passes something breaks without a word. In this post I will show you a short Microsoft Graph PowerShell script that lists every app credential in your tenant, sorted by how many days it has left, so you find the problem before your users do.
What you need
This is read only. You are auditing, not changing anything, so you do not need write access to run it. You also do not need the whole Graph SDK, just two sub-modules.
Install-Module Microsoft.Graph.Authentication, Microsoft.Graph.Applications -Scope CurrentUser Connect-MgGraph -Scopes "Application.Read.All"
The scope is Application.Read.All. If you already use PnP PowerShell for SharePoint, run this in a separate window. PnP and the Graph SDK both bundle the same authentication assembly and clash when they are loaded together.
The script
One thing to understand before the code. An app registration can hold more than one credential. Secrets live in PasswordCredentials, certificates in KeyCredentials, and an app can have several of each, some expired and some not. You have to flatten both lists into one row per credential before you sort, otherwise you see the app but miss which secret is the dead one.
$now = Get-Date
Get-MgApplication -All -Property DisplayName,AppId,PasswordCredentials,KeyCredentials |
ForEach-Object {
$app = $_
foreach ($c in @($app.PasswordCredentials) + @($app.KeyCredentials)) {
if ($c.EndDateTime) {
[pscustomobject]@{
App = $app.DisplayName
Type = if ($c.Hint) { 'Secret' } else { 'Certificate' }
Expires = $c.EndDateTime.ToString('yyyy-MM-dd')
DaysLeft = [math]::Round(($c.EndDateTime - $now).TotalDays)
}
}
}
} | Sort-Object DaysLeft | Format-Table -AutoSize
The line worth knowing is if ($c.Hint). A client secret carries a Hint, which is the first few characters of the secret. A certificate does not. That is the simplest way to label each row as a secret or a certificate without a second lookup.
Reading the output
Here is what came back on one of my tenants. The app names are changed, the dates and day counts are exactly what the script returned.
App Type Expires DaysLeft --- ---- ------- -------- Contoso-LegacyReporting Certificate 2021-09-12 -1790 Contoso-ConsoleReader Secret 2023-03-28 -1228 Contoso-ConsoleReader Secret 2024-07-18 -750 Contoso-DevReader Secret 2024-07-18 -750 Contoso-SitesReadWrite Secret 2024-10-10 -666 Contoso-SitesReadWrite Certificate 2025-04-13 -481 Contoso-SitesReadWrite Secret 2025-09-29 -312 Contoso-TicketsConnector Secret 2026-05-03 -96 Contoso-WikiConnector Secret 2026-05-03 -96 Contoso-WikiConnector Secret 2026-05-08 -91 Contoso-FieldCustomizer Certificate 2027-05-14 280 SharePoint Online Client Extensibility Web Application Principal Secret 2072-09-16 16842
Read it top to bottom, worst first.
- Every negative number already expired. The top one expired 1790 days ago, which is nearly five years. Either that app is dead and should be removed, or something is quietly failing against it right now. Both are worth knowing.
- The two connector secrets in the middle expired about three months ago. That is exactly the kind of thing that breaks an integration and gets blamed on everything except the secret.
- The small positive number is the one to act on. A certificate with 280 days left is fine for now, but that date goes in the calendar today, not next year.
- The last row, expiring in 2072, is the built in SharePoint Online Client Extensibility Web Application Principal. Microsoft creates it with a fifty year secret. Leave it alone.
Don’t forget service principals
App registrations are only half the picture. Enterprise applications, the service principals, carry their own credentials too. Swap Get-MgApplication for Get-MgServicePrincipal in the same script and you cover those as well. Between the two you see every credential in the tenant that has an expiry date on it.
The fix is boring and it works. Rotate the credential before the date, and put the next expiry somewhere you will actually look. Run this by hand every month, or schedule it and mail yourself anything under thirty days. Find the expiry on your terms, not because a connector went dark.