Secrets might not make friends, but they’re really useful for locking down your app registrations in Entra. Although you might be tempted to go ahead and set them to expire 25 years into the future (if it was possible…is it possible???) so you never have to think about it again, your security department might have other ideas. Sys Admins hate anything that expires. Certs? Hate ’em. Milk? Hate it. “Fresh” gas station sushi? Gimme gimme gimme! One way to deal with expiring client secrets is to wait and listen for the app owner’s scream. To be fair, this is a solution, but let’s take a gander at how we check the expiration of client secrets (or certs) for Entra apps. You know—before they expire.

\\\GTG Get The Graph

First, you gotta get the Graph SDK. It was forced upon us by the MS overlords, but alas, here we are embracing our roles as peasants under such cruel tyranny. Connect to the Graph with the Application.Read.All scope so you can view apps. Thankfully, this one is pretty straightforward. Use Get-MgApplication to get all of the apps. In this example, I’m going to store it in a variable called $Apps:

$Apps = Get-MgApplication
That’s it. We’re done. Thanks for coming to my TED Talk.

Turns out there’s a bit more to do but the certs and secrets are within reach. Certs show up in the KeyCredentials property and secrets show up in PasswordCredentials. We can get the app to spill the beans and see if it has either (or both) by selecting these properties:

$Apps | select DisplayName, KeyCredentials, PasswordCredentials

\\\Days of Future Past

Now if there is a secret or a cert, you can check the EndDateTime property within each credential property to see when the apocalypse is going to happen. But what if you’re not a mathologist and don’t have enough fingers or toes to figure out how long that is? Well put down that abacus and let’s let PowerShell calculate how many days we have left for us.

For the purposes of demonstration, I’ll grab the first app that has a client secret using Where-Object (alias where) and referencing the first in the index ([0]):

($Apps | where {$_.PasswordCredentials})[0].PasswordCredentials.EndDateTime
Friday, May 8, 2026 9:32:17 AM

We can use GetType() to see that this is indeed a DateTime data type:

($Apps | where {$_.PasswordCredentials})[0].PasswordCredentials.EndDateTime.GetType()
IsPublic IsSerial Name      BaseType
-------- -------- ----      --------
True     True     DateTime  System.ValueType

One of the great things about DateTime data types is that they can easily be subtracted from each other. Let’s take the value of when our secret expires and subtract it from today’s date. Then we can just select the number of days from it to get our answer:

$DaysLeft = (($Apps | where {$_.PasswordCredentials})[0].PasswordCredentials.EndDateTime - (Get-Date)).Days
734

That’s great and all, but how can we make this more practical to use on a regular basis or even automate? And what if we have billions of apps in Azure? <rant> Pretty sure I’m going to secretly call it Azure for the rest of my life. I’ve had all the forced rebranding I can handle! I remember what you did to Skype For Business/Lync/Office Communicator/Live Communicator/tin can phone! </rant> Let’s try this again. What if we have billions of apps in Entra? Let’s make this even better by creating a function that does the math for us and only gets the apps which have a secret/cert expiring in the next 30 days.

\\\We’re Not Done

First, if you need a deeper look at functions in PowerShell, take a look at my posts that go over various aspects of functions. Essentially, we’re going to loop through each app, and then loop through each cert and each secret (since you can have multiple of each) within each app. Then if one cert or secret is going to expire in our specified time period, we’ll output the app. I like to start with an outline so you can get an idea of where we’re at and where we’re going. Here’s the rough idea of what we’re trying to do:

function Get-ExpiringEntraApps{
    [cmdletbinding()]
    Param(
        $DaysUntilExpiration
    )
    Begin{
        $AppRegistrations = Get-MgApplication
    }
    Process{
        $ExpiringApps = @()
        foreach($App in $AppRegistrations){
            
            #Check Expiring Certs
            #Check Expiring Secrets
        }
        $ExpiringApps
    }
}
Both sections will be very similar, but let’s walk through the first section and see what we’re working with. $App is the iteration of each app in $AppRegistrations, and for our foreach loop for checking the certs, we’ll call each iteration of certs $Key. We’ll write an if statement to check if the EndDateTime minus the current date is less than or equal to the $DaysUntilExpiration we have set as a parameter in our function.

If the cert is going to expire within our specified time period, we’ll create a new PSCustomObject with the CertName and the DaysLeft. We’ll add this to our empty array we created for $ExpiringCerts that’s outside of the foreach loop.

Finally, we’ll use Add-Member to add a property to the $App with all of the $ExpiringCerts. We do this so we can store multiple expiring certs in case there’s more than one. You know, just in case you went psycho and created a lot of certificates. Here’s what all of this looks like put together:

#Check Expiring Certs
$ExpiringCerts = @()
foreach($Key in $App.KeyCredentials){
    $DaysLeft = ($Key.EndDateTime - (Get-Date)).Days
    if($DaysLeft -le $DaysUntilExpiration){
        $ExpiringCerts += [PSCustomObject]@{
            CertName = $Key.DisplayName
            DaysLeft = $DaysLeft
        }
    }
    rv DaysLeft -ErrorAction SilentlyContinue
}
$App | Add-Member -MemberType NoteProperty -Name 'ExpiringCerts' -Value $ExpiringCerts -Force
One thing to note here is that I add in rv (alias for Remove-Variable) just in case there’s something that goes awry and it uses the previous stored value. This one has bitten me a few times before so I throw this in there just as a precaution.

\\\Finishing Touches

Now that we got certs out of the way, we’re going to repeat almost the same thing for secrets. We’ll change a few of the variable names around and add another property for the secret hint, but it’s basically tomato potato. Here’s what it looks like:

#Check Expiring Secrets
$ExpiringSecrets = @()
foreach($Password in $App.PasswordCredentials){
    $DaysLeft = ($Password.EndDateTime - (Get-Date)).Days
    if($DaysLeft -le $DaysUntilExpiration){
        $ExpiringSecrets += [PSCustomObject]@{
            SecretName = $Password.DisplayName
            SecretHint = $Password.Hint
            DaysLeft = $DaysLeft
        }
    }
    rv DaysLeft -ErrorAction SilentlyContinue
}
$App | Add-Member -MemberType NoteProperty -Name 'ExpiringSecrets' -Value $ExpiringSecrets -Force
Last thing we’ll add is right after our certs and secrets foreach loops, we’ll write an if statement to only add the app to our $ExpiringApps array if does indeed have an expiring cert and/or secret. This little snippet looks like this:
if($App.ExpiringSecrets -OR $App.ExpiringCerts){
    $ExpiringApps += $App
}
rv ExpiringCerts, ExpiringSecrets -ErrorAction SilentlyContinue

Putting it all together, here’s our finished function in all of it’s secret-telling glory (you can also get this on GitHub):

Get-ExpiringEntraApps
function Get-ExpiringEntraApps{
    [cmdletbinding()]
    Param(
        $DaysUntilExpiration
    )
    Begin{
        $AppRegistrations = Get-MgApplication
    }
    Process{
        $ExpiringApps = @()
        foreach($App in $AppRegistrations){
            
            #Check Expiring Certs
            $ExpiringCerts = @()
            foreach($Key in $App.KeyCredentials){
                $DaysLeft = ($Key.EndDateTime - (Get-Date)).Days
                if($DaysLeft -le $DaysUntilExpiration){
                    $ExpiringCerts += [PSCustomObject]@{
                        CertName = $Key.DisplayName
                        DaysLeft = $DaysLeft
                    }
                }
                rv DaysLeft -ErrorAction SilentlyContinue
            }
            $App | Add-Member -MemberType NoteProperty -Name 'ExpiringCerts' -Value $ExpiringCerts -Force
            #Check Expiring Secrets
            $ExpiringSecrets = @()
            foreach($Password in $App.PasswordCredentials){
                $DaysLeft = ($Password.EndDateTime - (Get-Date)).Days
                if($DaysLeft -le $DaysUntilExpiration){
                    $ExpiringSecrets += [PSCustomObject]@{
                        SecretName = $Password.DisplayName
                        SecretHint = $Password.Hint
                        DaysLeft = $DaysLeft
                    }
                }
                rv DaysLeft -ErrorAction SilentlyContinue
            }
            $App | Add-Member -MemberType NoteProperty -Name 'ExpiringSecrets' -Value $ExpiringSecrets -Force
            if($App.ExpiringSecrets -OR $App.ExpiringCerts){
                $ExpiringApps += $App
            }
            rv ExpiringCerts, ExpiringSecrets -ErrorAction SilentlyContinue
        }
        $ExpiringApps
    }
}

And thankfully, using this function is as easy as building anything from IKEA:

Get-ExpiringEntraApps -DaysUntilExpiration 30
Okay, one more thing. You can pipe the output to Format-List if you want to see all of the properties, or you can pipe to Select-Object to choose what you want. Optionally, you can select the properties you want ahead of time in the function if you know what you want. I left it open so you could choose your own ending!

OTHER POSTS YOU WANT TO READ

Disable O365 Apps in License (PowerShell)

If it's free, it's better. This is basically my mantra for life and a really easy one to follow at that. You can apply it to all sorts of things: ice cream, candy, food. Okay, maybe just free food tastes better when you know you didn't have to pay for it. Especially...

Block Direct Delivery to onmicrosoft.com (Exchange Hybrid)

This post may/may not include actual sound parenting advice. Don't get triggered.You're the proud parent of a bustling little rascal who is growing faster than you can stand it. This kid is now becoming an adult and it's time to start earning that keep. You've been...

Existing O365 Cloud Accounts – Sync Errors

The word cloud means different things to different folks. To some, it's a term of endearment and something they hold close to their heart. Some only think it's a light fluffy thing in the sky that can sometimes appear to make funny shapes. And still some think of it...

OneDrive Retention Conspiracy

Before I dive into the OneDrive Retention shenanigans, I just want to go on the record and say: I love documentation. At least, I enjoy reading superbly written documentation when it comes to technical things like computers and things that compute and go pew pew. Now,...

Exchange Online – Recipient Filters for Dynamic Distribution Groups

Who doesn’t love Exchange Online dynamic distribution groups? It's like cattle that sort themselves. Or maybe you don’t love them, but who loves maintaining super large distribution groups? (Not this guy) It’s like feeling good about who you are as a person when...