All projects

Credential expiry monitoring for Entra ID app registrations

The problem
Secrets and certificates across the tenant expired without warning; nobody knew who owned the apps.
What I built
An owner for every app, a sign-in filter, and a plain-language notice 30 and 7 days before expiry.
What came of it
Owners act before anything breaks; integrations stopped failing on Monday mornings.
Credential expiry notification flowPowerShellLogic AppInventoryapps and credentialsSign-in matchin use or forgottenLogic App30 and 7 days beforeSummaryAzure OpenAImetadata, never secretsOwnerplain-language noticerenew or retireExpired > 30 daysdecommission script-Execute off by default

An Entra ID tenant accumulates app registrations the way a garage accumulates cables: integrations, internal tools, vendor connectors, something someone set up in 2019 and left. Each one carries a client secret or a certificate. Each of those has an expiry date. When it passes, the integration stops. A ticket arrives a few days later, and the first hour goes into working out who owns the app and what it was for.

In this tenant there were more of them than there was a reliable count of, which, looking back, was already part of the problem.

The first instinct is to ask Graph which credentials expire soon and mail the list somewhere, and the Entra portal already filters app registrations by expiring credentials, so that part was never the problem. What it gets you is an alert nobody acts on, and before the first notice was worth sending there were two questions I had to answer.

  • Who owns it? One accountable person per application, written on the object.

  • Is it in use? Only credentials with recent sign-ins deserve an alert.

  • Then the notice. 30 and 7 days ahead, in words the owner can act on.

Question one: who owns this?

Entra ID does not require an application to have an owner, and it will happily let you create one without ever naming a responsible person. Predictably, a large share of these had no owner recorded, or one who had left the company years earlier.

A notice with nowhere to go is worthless, so half the project was establishing one accountable contact per application. The sources that actually work, in rough order of reliability: the team that consumes the integration, the directory’s audit trail of who created and last modified the registration, and whoever holds the subscription or resource the app talks to.

The part that matters for the long run is writing the answer back onto the registration itself. Ownership discovered and left in a spreadsheet decays within a year. Ownership written to the object is found by the next notification without anyone asking again.

Question two: is this credential actually in use?

The second filter is just as important, and it is the one most implementations skip.

A PowerShell script reads every application and every credential through Microsoft Graph, then matches each credential against service principal sign-in activity from recent months. That separates “in use” from “forgotten”, and the distinction changes what the alert means: an expiring credential on a dead integration is a clean-up item, not an incident waiting to happen.

So notifications go out only for credentials with recent sign-in activity. A stale secret sitting beside an active certificate raises nothing. The same pass surfaced dozens of credentials that had already expired, some by years: evidence, incidentally, that nothing was using them. A companion script retires those, with a dry run by default and a backup of every object before it touches anything.

The notice, and who reads it

A Logic App writes to the app owner 30 and 7 days before expiry. Worth remembering who that owner usually is: a product owner or a business contact, not an engineer.

If the mail says your credential keyId 3f2a… with permission Directory.Read.All and redirect URI https://… expires on the 14th, they forward it to IT and the ticket arrives anyway. You have automated the discovery and kept all of the work.

All projects