HomeNewsletterCommunityToolsArchiveBlogToday's NewsAboutServicesQuick Links Subscribe free
← Back to Blog

How Many of Your App Registrations Have a Client Secret Expiring Next Week - and Does Anyone Actually Know?

IA
Imran Awan
22 July 2026

How many of your app registrations have a client secret expiring next week - and does anyone actually know? Not "does someone know it will happen eventually." Does anyone, right now, have a list with a date on it? For most tenants the honest answer is no. There is no default email, no built-in nag, no dashboard tile that says "this integration dies in six days." The secret just quietly stops working, at whatever hour it happens to cross its expiry timestamp, and the first anyone hears about it is a broken integration and a confusing error in a log file.

This post explains what an app registration and a client secret actually are in plain English, why expiry tracking falls through the cracks even in well-run tenants, how to manually check one app's credentials before you trust any script's output, and then gives you a complete PowerShell script that audits every app registration in the tenant and tells you exactly which credentials are dead, dying, or fine.

Download the script β€” GitHub

Get-AppRegistrationExpiryReport.ps1 is free and open source. Download it from the Imran76Awan/Daily-Tasks repository on GitHub - no sign-in required. This script is read-only - it audits and reports, it never rotates, deletes, or modifies a credential.

Get-AppRegistrationExpiryReport.ps1 View full repo β†’

The problem: a secret expires and nothing announces it

Before anything else, two definitions, because the rest of this post is meaningless without them. An app registration is Entra ID's record of an application that needs to talk to Microsoft Graph, sign users in, or call an API - think of a payroll integration, a monitoring tool, or an internal script that pulls device data. When you register that application, Entra ID gives it an application (client) ID, a GUID - a globally unique identifier, a 128-bit value written as 32 hex characters in five dashed groups - that identifies the app the way a username identifies a person.

A client secret is one way that app proves it really is that app when it authenticates - functionally, a password that Entra ID generated for the application rather than a human. A certificate credential does the same job with a public/private key pair instead of a shared secret. Either way, Entra ID stamps every credential with a start date and an end date when it is created. Nothing forces that end date to be more than two years away. Nothing forces anyone to write it down anywhere else. Nothing sends a warning email by default.

So here is the symptom, verbatim, that a reader sees the moment the last valid credential on an app crosses its expiry date and something tries to authenticate with it:

Token request failure — AADSTS7000215
AADSTS7000215: Invalid client secret provided. Ensure the secret being sent
in the request is the client secret value, not the client secret ID, for a
secret added to app 'a1b2c3d4-e5f6-4789-a0b1-c2d3e4f56789'.

Trace ID    : 8f2e1a90-6c3d-4b5a-9e7f-1a2b3c4d5e6f
Correlation : 4d5c6b7a-8e9f-0a1b-2c3d-4e5f60718293
Timestamp   : 2026-07-22 06:14:02Z

Notice the wording. Entra ID's error text nudges you toward the wrong diagnosis first - "check you're sending the value, not the ID" - because that is the more common mistake when a secret is freshly created and simply pasted in wrong. But when the integration has been running fine for eighteen months and suddenly starts throwing AADSTS7000215 overnight with no code change, the far more likely cause is that the secret simply ran out the clock. Nobody typed anything wrong. Time just passed.

πŸ“‹ Note: This is a pure Entra ID / cloud identity control. There is no Group Policy path and no Intune CSP / OMA-URI for app registration credential lifecycle - app registrations exist in the directory, not on a managed device, so device management tooling has no visibility into them at all. Also worth being precise about scoping: this post, and the script, work against app registrations (the application object, tenant-wide definition) and their credentials. A related but distinct object, the service principal, is the per-tenant instance of that application used at sign-in time - relevant if the app is multi-tenant - but the PasswordCredentials and KeyCredentials that actually expire live on the application object, which is exactly what this script targets.

Why it happens: multiple credentials pile up and nobody owns the calendar

The structural root cause is that Entra ID lets - and in practice, encourages - an app registration to hold more than one credential at once. Under Certificates & secrets in the portal, "Add a client secret" and "Upload a certificate" are both repeatable actions with no limit that a normal admin will ever hit. Teams rotate secrets by adding a new one alongside the old one, intending to delete the old one once the new one is confirmed working - and then the old one just sits there, expired, forever, because deleting it felt risky and nobody circled back.

Multiply that pattern across a tenant with a few hundred app registrations, built up over several years by different teams, and you get exactly what the Microsoft Graph data model actually returns: a PasswordCredentials array and a KeyCredentials array on every application object, each one potentially holding several entries, each entry with its own independent StartDateTime and EndDateTime. Checking "does this app have a secret" tells you nothing useful. You have to check every single entry in both arrays, on every single app, individually.

Compounding that, there is no built-in, tenant-wide expiry-warning mechanism that fires reliably before something breaks. Microsoft does surface some in-portal nudges and, in some configurations, notification emails to app owners as a credential nears expiry - but those depend on an app owner being correctly set, on someone actually reading that inbox, and on the notification not landing in a shared mailbox nobody checks. There is no guaranteed, centralized, admin-visible worklist of "every credential expiring across the whole tenant in the next 30 days" anywhere in the Entra admin center. That gap is exactly what this post's script fills.

⚠ Gotcha: An app registration can look completely healthy - one valid, non-expiring-soon secret sitting right there in the Certificates & secrets blade - while several other credentials on that same app are already expired and have been for months. The portal does not warn you about the dead ones just because a live one exists alongside them. If your team's habit is "open the app, see a green secret, move on," you are systematically missing every credential that isn't the newest one. This is precisely why the script below checks every credential in both arrays, not just whether the array is non-empty.

How to verify - check one app by hand before you trust any script

Before running anything tenant-wide, it is worth knowing how to manually confirm the finding on a single app, because that is exactly what you or a colleague will do to sanity-check the script's output the first time it flags something important.

  1. Sign in to the Microsoft Entra admin center with at least Application Administrator or Cloud Application Administrator rights
  2. Go to Identity › Applications › App registrations
  3. Select the app you want to check - for example, a fictional integration called Contoso Expense App
  4. Select Certificates & secrets in the left-hand menu - this shows two tabs: Certificates and Client secrets
  5. On the Client secrets tab, read the Expires column for every single row, not just the first one

Here is what that tab looks like on a real, un-remediated app - three secrets, only one of them actually usable:

Entra admin center β€” App registrations β€” Contoso Expense App β€” Certificates & secrets
Client secrets (3) Certificates (0)
Prod secret 2023
Created 04 Jan 2023 · Expires 04 Jan 2025
Expired
Prod secret 2025 rotation
Created 02 Jan 2025 · Expires 26 Jul 2026
Expires in 4 days
Backup secret (unused)
Created 11 Mar 2024 · Expires 11 Mar 2026
Expired

Three rows on one app, and the picture is not reassuring. One secret expired eighteen months ago and was clearly never cleaned up. A second one, added as a supposed backup, also expired four months before this screenshot was taken and nobody noticed. The only secret keeping this integration alive expires in four days. Checking this by hand, for this one app, took under two minutes once you knew where to look. Doing that same check across every app registration in a tenant that might have several hundred of them is not realistic without a script - which is exactly why the next section exists.

The fix - Get-AppRegistrationExpiryReport.ps1

The script uses the Microsoft.Graph.Applications PowerShell module to do programmatically, across the entire tenant, exactly what you just did by hand for one app. It calls Get-MgApplication -All to retrieve every app registration, then walks the PasswordCredentials and KeyCredentials arrays on each one, comparing every single credential's EndDateTime against the current date to classify it as Expired, Expiring, or Healthy.

Get-AppRegistrationExpiryReport.ps1 — core detection logic
# Connect with the read-only Graph scope needed to read app credentials
Connect-MgGraph -Scopes 'Application.Read.All' -UseDeviceCode -NoWelcome

# Every app registration in the tenant, with its credential arrays
$apps = Get-MgApplication -All -Property "Id,AppId,DisplayName,PasswordCredentials,KeyCredentials"

$now = Get-Date

foreach ($app in $apps) {

    # Check EVERY secret, not just whether the array is non-empty
    foreach ($secret in $app.PasswordCredentials) {
        $daysToExpiry = [math]::Round(($secret.EndDateTime - $now).TotalDays)

        if ($daysToExpiry -lt 0)          { $status = 'Expired' }
        elseif ($daysToExpiry -le $WarningDays) { $status = 'Expiring' }
        else                                { $status = 'Healthy' }

        # ...added to $results with AppDisplayName, EndDateTime, Status, etc.
    }

    # Certificates go through the identical check against KeyCredentials
    foreach ($cert in $app.KeyCredentials) {
        $daysToExpiry = [math]::Round(($cert.EndDateTime - $now).TotalDays)
        # ...same classification, same output row
    }
}

The script requires only the Application.Read.All Graph permission - a read-only application permission. It never writes anything back to Entra ID. It supports two connection modes: pass -TenantId, -ClientId, and -CertificateThumbprint for unattended app-only certificate authentication - the right choice for a scheduled task or an Azure Automation runbook - or omit them entirely and it falls back to an interactive device-code sign-in with the same read-only scope. It also always disconnects and reconnects fresh rather than trusting a stale cached session, and it fails loudly with Write-Error and a non-zero exit code on any connection or query failure, rather than silently reporting zero findings, which would look identical to a genuinely clean tenant. Here is a realistic run against a test tenant, sorted worst-first by days to expiry:

Get-AppRegistrationExpiryReport.ps1 — before remediation
[INFO] Connecting to Microsoft Graph...
[INFO] Connected. Tenant: β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ-β–ˆβ–ˆβ–ˆβ–ˆ-β–ˆβ–ˆβ–ˆβ–ˆ-β–ˆβ–ˆβ–ˆβ–ˆ-β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ  Account: i.awan@contoso.com

=== Retrieving app registrations ===
[INFO] Found 214 app registration(s) in the tenant.

=== Findings ===

[EXPIRED ] Secret       Contoso Expense App                      Prod secret 2023             ends 2025-01-04  (-564 day(s))
[EXPIRED ] Secret       Contoso Expense App                      Backup secret (unused)       ends 2026-03-11  (-133 day(s))
[EXPIRED ] Certificate  Contoso Data Sync Connector               sync-cert-2024               ends 2026-06-30  (-22 day(s))
[EXPIRING] Secret       Contoso Expense App                      Prod secret 2025 rotation    ends 2026-07-26  (4 day(s))
[EXPIRING] Secret       Contoso Helpdesk Bot                      Client secret                ends 2026-08-09  (18 day(s))
[HEALTHY ] Secret       Contoso Payroll Export                   Client secret                ends 2027-02-15  (208 day(s))

=== Summary ===
Total app registrations scanned : 214
Total credentials evaluated      : 389
Expired credentials              : 11
Expiring within 30 day(s)         : 6
Healthy credentials               : 372

[INFO] Audit complete.

Eleven expired credentials sitting in the tenant right now, unnoticed, and six more due to fail within the month - including that Contoso Expense App secret with four days left, the exact scenario this post opened with. Two of the expired ones on that same app are the dead secrets from the manual check earlier - now confirmed programmatically instead of by clicking through the portal one app at a time.

⚠ Warning: Do not delete or rotate a flagged secret or certificate without first confirming nothing still depends on it. An "expired" credential that nobody bothered to clean up is usually safe to delete - it cannot authenticate anything anyway. But an "expiring" credential that is still valid right now may be actively in use by a production integration you don't have full visibility into. Check with the app owner, check recent Entra ID sign-in logs filtered to that application, and only then rotate. Killing a still-working secret on the wrong app is how you turn a proactive audit into an unplanned outage.

Once a credential is confirmed genuinely dead or genuinely at risk, remediation is the same short sequence every time:

Remediation sequence β€” per flagged credential
1
Identify the app owner
In the app registration's Owners blade, or your CMDB, find who is responsible for the integration this credential belongs to.
2
Add the replacement secret or certificate first
In Certificates & secrets, add the new credential and update the integration's configuration to use it before touching the old one.
3
Confirm the integration authenticates with the new credential
Trigger a real sign-in or token request and check the Entra ID sign-in logs for a successful entry against the new KeyId before assuming it worked.
4
Delete the expired or replaced credential
Once confirmed, remove the old entry from Certificates & secrets so it stops cluttering future audit runs.
5
Set a shorter expiry and a calendar reminder
Prefer 6 or 12 month expiries over the default 24 months, and put the rotation date on a team calendar or ticketing system rather than trusting memory.

Proof it worked - rotate a flagged secret and re-run the audit

Take the Contoso Expense App secret with four days left, rotate it following the sequence above, and delete the two dead secrets alongside it while you're in there. Re-run the exact same script with no other changes:

Get-AppRegistrationExpiryReport.ps1 — after remediation
[INFO] Connecting to Microsoft Graph...
[INFO] Connected. Tenant: β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ-β–ˆβ–ˆβ–ˆβ–ˆ-β–ˆβ–ˆβ–ˆβ–ˆ-β–ˆβ–ˆβ–ˆβ–ˆ-β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ  Account: i.awan@contoso.com

=== Retrieving app registrations ===
[INFO] Found 214 app registration(s) in the tenant.

=== Findings ===

[EXPIRED ] Certificate  Contoso Data Sync Connector               sync-cert-2024               ends 2026-06-30  (-22 day(s))
[EXPIRING] Secret       Contoso Helpdesk Bot                      Client secret                ends 2026-08-09  (18 day(s))
[HEALTHY ] Secret       Contoso Expense App                      Prod secret 2026 rotation    ends 2028-07-22  (730 day(s))
[HEALTHY ] Secret       Contoso Payroll Export                   Client secret                ends 2027-02-15  (208 day(s))

=== Summary ===
Total app registrations scanned : 214
Total credentials evaluated      : 387
Expired credentials              : 9
Expiring within 30 day(s)         : 5
Healthy credentials               : 373

[INFO] Audit complete.

Contoso Expense App has dropped off both the Expired and Expiring lists entirely - the two dead secrets were deleted, and the four-day secret was replaced with a fresh one now healthy for two years. The credential count on that one app went from three to one, and the total evaluated count in the tenant dropped from 389 to 387 to match. The remaining findings - the certificate that expired 22 days ago on a different app, and the helpdesk bot secret with 18 days left - are exactly what should still be there, since this remediation pass only touched the one app.

βœ… Tip: Don't run this once and call it done. Schedule Get-AppRegistrationExpiryReport.ps1 to run weekly via an Azure Automation runbook using app-only certificate authentication, with -ExportCsv enabled and the CSV dropped somewhere your team actually looks - a SharePoint list, a Teams channel via webhook, or a ticketing system import. New app registrations get created constantly, each one starts a fresh expiry clock, and a one-time clean-up only buys you until the next rotation deadline nobody wrote down.
💼 Need this done for you?
Want your app registration expiry audit run by someone who's done this before?

I'll audit every app registration in your tenant, flag every expired and expiring credential, and help you build a rotation process your team will actually keep up with.

💬 See how I can help → 📅 Book a free call

References

βœ… Bottom line: A client secret expiring is not a security incident and not a bug - it is a calendar event that nobody put on a calendar. Entra ID will not warn your whole team by default, and the portal will not stop you from thinking an app is fine just because one of its several credentials still works. A weekly, scripted, tenant-wide check is the only reliable way to see the expiry date before your integration does.

So - how many of your app registrations have a client secret expiring next week? Run the script above and find out before an on-call page does it for you.

Share this post
LinkedIn X / Twitter Reddit Bluesky

More from EndpointWeekly

Security
That AI Tool You Just Gave 'Read/Write All' to Entra and Intune…
A newly adopted AI ITSM platform just asked for Directory.ReadWrite.All. Most tenants…
Entra ID
How Many of Your Global Admins Are PERMANENTLY Global…
Standing privileged access is the top finding in every security review β€” and turning…
Entra ID
Do You Know Which Users Have ZERO Conditional Access Policy…
Conditional Access targets groups and apps, not "all users" by default. Gaps open…