Find expiring Entra app secrets and certificates with PowerShell
|

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.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.