How to find expiring app registration secrets with PowerShell
Published
An integration that signs in to Microsoft 365 with a client secret stops working the minute that secret expires, and nothing in the tenant warns you first. The fix is a list of every secret and certificate with its end date and owner, read before the date rather than after the outage.
What expires?
Every app registration in Entra ID can hold two kinds of credential, and each has an end date:
- Client secrets (
passwordCredentials). The admin center offers lives of up to 24 months. Microsoft never shows the value again, but the name and the dates stay readable. - Certificates (
keyCredentials). The public part of a certificate uploaded to the app. Its end date is the certificate’s own expiry.
Enterprise applications that use SAML single sign-on are a separate case. Their signing certificate lives on the service principal, not the app registration, and its end date is in preferredTokenSigningKeyEndDateTime. A short query for those follows the main script.
How do you list them with Microsoft Graph PowerShell?
Reading app registrations needs one delegated scope, Application.Read.All, consented once by an administrator. The account that runs the script needs a role that can read applications, such as Global Reader. Install the module with Install-Module Microsoft.Graph.Applications -Scope CurrentUser if you do not have it.
# Written for the Microsoft Graph PowerShell SDK v2 (Microsoft.Graph.Applications). Run it first against a test tenant.
Connect-MgGraph -Scopes "Application.Read.All" -NoWelcome
$now = [datetime]::UtcNow
$apps = Get-MgApplication -All -Property "id,appId,displayName,passwordCredentials,keyCredentials"
$rows = foreach ($app in $apps) {
if (-not ($app.PasswordCredentials -or $app.KeyCredentials)) { continue }
$owners = Get-MgApplicationOwner -ApplicationId $app.Id -All | ForEach-Object {
if ($_.AdditionalProperties.userPrincipalName) { $_.AdditionalProperties.userPrincipalName }
else { $_.AdditionalProperties.displayName }
}
$creds = @($app.PasswordCredentials | Select-Object @{n='Type';e={'Secret'}}, DisplayName, EndDateTime) +
@($app.KeyCredentials | Select-Object @{n='Type';e={'Certificate'}}, DisplayName, EndDateTime)
foreach ($c in $creds) {
[pscustomobject]@{
Application = $app.DisplayName
AppId = $app.AppId
Type = $c.Type
Credential = $c.DisplayName
Expires = $c.EndDateTime
DaysLeft = [math]::Floor(($c.EndDateTime.ToUniversalTime() - $now).TotalDays)
Owners = ($owners -join '; ')
}
}
}
$rows = $rows | Sort-Object DaysLeft
$rows | Where-Object DaysLeft -le 90 | Format-Table Application, Type, Credential, DaysLeft, Owners -AutoSize
$rows | Export-Csv .\app-credentials.csv -NoTypeInformation -Encoding UTF8
A negative DaysLeft means the credential has already expired. Many of those are leftovers: someone added a new secret, moved the integration to it and never deleted the old one. If the same application has a newer credential of the same type with a later date, the expired one can probably go. Confirm with the owner first.
The same module and scope cover SAML certificates on enterprise applications:
Get-MgServicePrincipal -All -Property "displayName,preferredSingleSignOnMode,preferredTokenSigningKeyEndDateTime" |
Where-Object PreferredSingleSignOnMode -eq 'saml' |
Sort-Object PreferredTokenSigningKeyEndDateTime |
Select-Object DisplayName, PreferredTokenSigningKeyEndDateTime
What if you will not run a script?
In the Microsoft Entra admin center, go to Identity, Applications, App registrations, and choose All applications. The list shows the state of each application’s certificates and secrets, so expired and soon-to-expire ones stand out. Open an application and select Certificates & secrets to see each credential with its expiry date.
For SAML applications, go to Enterprise applications, open the application, select Single sign-on, and look at SAML Certificates. It shows the expiry and the notification email addresses Entra uses for that certificate. Check that those addresses still reach someone.
The admin center is fine for a dozen applications. Past that, clicking through each one is how a secret gets missed.
What do you do with the list?
Start at the top of the sorted CSV and give every row due in the next 90 days three things:
- An owner. The
Ownerscolumn shows who is listed in Entra. That person, or their team, renews it. - A renewal plan. Create the new secret or certificate, update the system that uses it, confirm it signs in, then delete the old credential. Keep the overlap short.
- A calendar date well before the expiry. Two weeks is a reasonable minimum for anything a vendor has to update.
Rows with an empty Owners column need the most attention. They are usually applications created by someone who has since left, and nobody will notice them until they break. Work out what the application does from its sign-in logs or its name, then assign an owner under App registrations, Owners. The same offboarding gap that orphans applications leaves licenses on disabled accounts; How to find unused Microsoft 365 licenses covers that side.
How do you make it recurring?
A list you run once is out of date the next time someone creates a secret. To run it on a schedule without your own sign-in:
- Create an app registration for the job, grant it the application permission
Application.Read.All, and have an administrator consent. - Give it a certificate, not a secret, and keep the private key on the machine that runs the job.
- Replace the first line of the script with
Connect-MgGraph -ClientId <app id> -TenantId <tenant id> -CertificateThumbprint <thumbprint> -NoWelcome. - Run it weekly from Task Scheduler, or from Azure Automation with a managed identity and
Connect-MgGraph -Identity, which needs no certificate at all.
If the job mails its results, note that the Mail.Send application permission can send as any mailbox unless you limit it with Exchange Online RBAC for Applications. And put the job’s own certificate on the list. The app that watches for expiring credentials has one too.
Checked against
- Microsoft.Graph.Applications 2.x (written against the v2 cmdlet and property names:
Get-MgApplication,Get-MgApplicationOwner,Get-MgServicePrincipal,PasswordCredentials,KeyCredentials,EndDateTime,PreferredSingleSignOnMode,PreferredTokenSigningKeyEndDateTime) - Microsoft.Graph.Authentication 2.x (
Connect-MgGraphwith-Scopes,-CertificateThumbprintand-Identity) - Windows PowerShell 5.1 and PowerShell 7.4; the script uses no operators that only PowerShell 7 has
Would you rather not maintain it?
If you would rather not keep the script and the schedule running, GlacierPoint Remind reads the same app registration secrets and certificates, SAML certificates and owners, and mails a warning 30, 14, 7 and 1 days before each one expires.