Find Expiring Enterprise Applications and App Registrations

ELEVATE KEYBOARD

So you have an Azure Tenant loaded with Enterprise Applications and App Registrations. It’d be a shame if they started to expire wouldn’t it? Oh wait, that’s exactly what they are going to do. And if you’re like me, you very logically figured you could generate some report or view some graph or anything in the Azure portal to give you an overview of this.

NOPE.

Sometimes the things Microsoft neglects to include are mind boggling. The only way to efficiently find what you need is with PowerShell. Your only other choice is to click through each Enterprise Application and App Registration one at a time and check what the dates are. Frankly, this limitation is ridiculous but here we are.

I’m honestly curious what the thought process was here

I’ve written myself a script that will pull all this information from Azure and dump it into a CSV file. Just as an FYI, it may be easier to view this code all at once instead of going through it section by section. Scroll to the bottom of the page and just copy / paste the whole thing into your PowerShell ISE if it’s easier for you. Anyway, let’s go! First, we have to get connected to Azure.

If($connection -eq $null){$connection = Connect-AzureAD}

You could simply run “Connect-AzureAD” of course, but by putting it in an If statement like I did, it won’t keep trying to reconnect if you re-run the script. This is probably mostly helpful for when you’re testing the script or making changes. You don’t want to have to enter your Azure credentials every time do you?

Alright so, in order to identify any Enterprise Application or App Registration that have an upcoming expiration date, you need to pull two different sets of data from Azure with the following commands. It’s possible to have expiring certificates for Enterprise Applications and / or expiring client secrets on App Registrations. These are two separate expirations and are both equally important, hence why running BOTH commands is important.

Get-AzureADServicePrincipal – This command will help you find expiration information for Enterprise Applications. You’ll see most guides on this matter reference this as the go to command. Yes, this will technically pull the expiration dates specifically for SSO certificates, but there’s more to it than that.

If you open Enterprise Applications in the Azure portal

Open one that is configured for Single sign-on and the click Single sign-on on the left

You’ll see this expiration date listed here. This is what we’re looking for with the Get-AzureADServicePrincipal command.

Get-AzureADApplication – This command will return App Registration information. Most of the service principals (Enterprise Applications) returned by the first command will have a corresponding App Registration. In addition, this command will also return App Registrations that do NOT have a service principal associated with it.

If you open Azure Active Directory in the Azure Portal

Go to App registrations on the left and then be sure to click All applications

And then choose one and click on Certificates & Secrets. You’ll see there’s another expiration date listed here (they won’t all have one, but this is what I mean, how are you supposed to know which ones do and which ones don’t?)

So it’s possible to have an AzureADServicePrincipal (Enterprise Application) with no App Registration, and it’s possible to have an App Registration with no AzureADServicePrincipal. “Vince, why is this a thing?” Sorry but I’m afraid I’m just as confused as you are, but this will at least help you identify stuff that’s going to expire.

First thing I do after connecting to Azure is to pull all the information using the two commands above

$ALL_AZADServicePrincipals = Get-AzureADServicePrincipal -all:$true | sort-object displayname
$ALL_AZADApplications = Get-AzureADApplication -all:$true | sort-object displayname

From here, I start by looping through each service principal that it found and identify all the expiration dates. Specifically, I’m looking at the PasswordCredentials property. It’s also possible to have more than one certificate associated with the Enterprise Application, so I’m also using the join command to add them all to a single line with a semicolon as the delimiter (this will make it easier to view in Excel later). In the event there is no certificate associated with the app, it will instead store a “null” value.

ForEach($AZADServicePrincipal in $ALL_AZADServicePrincipals){
$SPexpiration = ($AZADServicePrincipal | Select-Object -ExpandProperty passwordcredentials | Select-Object -ExpandProperty enddate | Sort-Object) -join ";"
If($SPexpiration -notlike "*/*"){$SPexpiration = $null}

Remember how I said the Enterprise Applications can have an App Registration associated with them? Here’s where I go through and identify the corresponding App Registration. This is done by matching up the AppID property. I start by clearing out the variable just in case it somehow has info from the previous loop, and then I check to see if there’s an App Registration with a matching AppID Property.

$AZADApplication = $null
$AZADApplication = $ALL_AZADApplications | Where-Object appid -eq $AZADServicePrincipal.appid

In the event it finds a match, I store the DisplayName, ObjectID, and AppID, and then go through and check to see if the App Registration has expiration information. Remember, the certificate associated with the Enterprise Application isn’t the only thing that can expire. If it doesn’t find a match, it stores “null” values for each variable instead.

If($AZADApplication -ne $null){

$ADDisplayname = $AZADApplication.displayname
$ADObjectID = $AZADApplication.objectID
$ADAppID = $AZADApplication.AppID


$ADClientSecretExpiration = $null
$ADClientSecretExpiration = ($AZADApplication | Select-Object -ExpandProperty passwordcredentials | Select-Object -ExpandProperty enddate | Sort-Object) -join ";"

If($ADClientSecretExpiration -notlike "/"){$ADClientSecretExpiration = $null}$ADCertExpiration = $null
$ADCertexpiration = ($AZADApplication | Select-Object -ExpandProperty KeyCredentials | Select-Object -ExpandProperty enddate | Sort-Object) -join ";"
If($ADCertexpiration -notlike "/"){$ADCertexpiration = $null}


}Else{


$ADDisplayname = $null
$ADObjectID = $null
$ADAppID = $null
$ADClientSecretExpiration = $null
$ADCertExpiration = $null
}

At the end of the ForEach loop I started, I call a function I’ve named AppArray which is used to build an array with all the information I’ve gathered. This looks long and scary but honestly it’s just passing a whole slew of variables along with the function call.

AppArray -SPDisplayname $AZADServicePrincipal.displayname -SPExpiration $SPexpiration -SPObjectID $AZADServicePrincipal.ObjectId -SPAppID $AZADServicePrincipal.AppId -ADDisplayname $ADDisplayname -ADObjectID $ADObjectID -ADAppID $ADAppID -ADClientSecretExpiration $ADClientSecretExpiration -ADCertExpiration $ADCertExpiration
} #this is the end of the foreach loop

The function it’s calling simply builds a custom object and then dumps it to an array named $AllApps. Here’s what the function looks like

Function AppArray($SPDisplayname,$SPExpiration,$SPObjectID,$SPAppID,$ADDisplayname,$ADObjectID,$ADAppID,$ADClientSecretExpiration,$ADCertExpiration){
$global:AllApps += @(
$AppObject = New-Object -TypeName psobject
$AppObject | Add-Member -MemberType NoteProperty -Name SPDisplayname -Value $SPDisplayname
$AppObject | Add-Member -MemberType NoteProperty -Name SPExpiration -Value $SPExpiration
$AppObject | Add-Member -MemberType NoteProperty -Name SPObjectID -Value $SPObjectID
$AppObject | Add-Member -MemberType NoteProperty -Name SPAppID -Value $SPAppID
$AppObject | Add-Member -MemberType NoteProperty -Name ADDisplayname -Value $ADDisplayname
$AppObject | Add-Member -MemberType NoteProperty -Name ADObjectID -Value $ADObjectID
$AppObject | Add-Member -MemberType NoteProperty -Name ADAppID -Value $ADAppID
$AppObject | Add-Member -MemberType NoteProperty -Name ADClientSecretExpiration -Value $ADClientSecretExpiration
$AppObject | Add-Member -MemberType NoteProperty -Name ADCertExpiration -Value $ADCertExpiration
$AppObject
)
}

At this point you’ve successfully gathered probably 95%+ of the info you were looking for, but if you recall I stated that it’s possible to have App Registrations that are not associated with an Enterprise Application. I’ve accounted for these outliers by running the $All_AZADApplications array we created earlier through a ForEach loop that checks each one to see if it’s already present in the $AllApps array. If it isn’t, it will then go through and add it by calling the same function I used earlier (AppArray).

ForEach($AZADApplication in $ALL_AZADApplications){
If($AllApps | Where-Object spappid -eq $AZADApplication.appid){}Else{


$ADClientSecretExpiration = $null

$ADClientSecretExpiration = ($AZADApplication | Select-Object -ExpandProperty passwordcredentials | Select-Object -ExpandProperty enddate | Sort-Object) -join ";"
If($ADClientSecretExpiration -notlike "/"){$ADClientSecretExpiration = $null}

$ADCertExpiration = $null
$ADCertexpiration = ($AZADApplication | Select-Object -ExpandProperty KeyCredentials | Select-Object -ExpandProperty enddate | Sort-Object) -join ";"
If($ADCertexpiration -notlike "/"){$ADCertexpiration = $null}
AppArray -SPDisplayname $null -SPExpiration $null -SPObjectID $null -SPAppID $null -ADDisplayname $AZADApplication.DisplayName -ADObjectID $AZADApplication.ObjectID -ADAppID $AZADApplication.AppID -ADClientSecretExpiration $ADClientSecretExpiration -ADCertExpiration $ADCertExpiration
}
}

And with that, you now have an array named $AllApps that has everything in it you need. From here you can easily export it to a CSV file using $AllApps | Export-Csv… This will give you a CSV containing all the Enterprise apps, their corresponding App Registrations, and any expiration dates. Now, because Microsoft includes a bunch of built-in applications that you probably don’t care about (and because this post is specifically about finding apps that can expire), you can trim down the list of results with the following

$AllApps | Where-Object {($_.spexpiration -ne $null) -or ($_.adexpiration -ne $null)}| Export-Csv -Path "<path>\<filename>.csv" -NoTypeInformation

This will now only include entries that have an expiration date for either the Service Principal or the App Registration. Oh, and if you haven’t figured it out by now, I used “SP” for info returned by the Get-AzureADServicePrincipal command and “AD” for info returned by the Get-AzureADApplication command.

Any expirations that have multiple values are a bit messy but I think you can figure it out. I would assume it’s a good idea to clear out old expired stuff anyway so maybe this is a good chance to identify anything with multiple values and clean it up.

And here’s the App Registrations that do not have an associated Enterprise Application / Service Principal. See how all the values for SPxxx are blank?

As promised, here’s the entire PowerShell script in one go. Copy & Paste it to your PowerShell ISE window to make it even easier to read.

Clear-Host

#This function builds the array
Function AppArray($SPDisplayname,$SPExpiration,$SPObjectID,$SPAppID,$ADDisplayname,$ADObjectID,$ADAppID,$ADClientSecretExpiration,$ADCertExpiration){
    $global:AllApps += @(
        $AppObject = New-Object -TypeName psobject
        $AppObject | Add-Member -MemberType NoteProperty -Name SPDisplayname -Value $SPDisplayname
        $AppObject | Add-Member -MemberType NoteProperty -Name SPExpiration -Value $SPExpiration
        $AppObject | Add-Member -MemberType NoteProperty -Name SPObjectID -Value $SPObjectID       
        $AppObject | Add-Member -MemberType NoteProperty -Name SPAppID -Value $SPAppID
        $AppObject | Add-Member -MemberType NoteProperty -Name ADDisplayname -Value $ADDisplayname
        $AppObject | Add-Member -MemberType NoteProperty -Name ADObjectID -Value $ADObjectID
        $AppObject | Add-Member -MemberType NoteProperty -Name ADAppID -Value $ADAppID
        $AppObject | Add-Member -MemberType NoteProperty -Name ADClientSecretExpiration -Value $ADClientSecretExpiration
        $AppObject | Add-Member -MemberType NoteProperty -Name ADCertExpiration -Value $ADCertExpiration
        $AppObject
    )
}

#Connects to Azure
If($connect -eq $null){$connect = Connect-AzureAD}

#Pulls all the Service Principals and AD Applications
$ALL_AZADServicePrincipals = Get-AzureADServicePrincipal -all:$true | sort-object displayname
$ALL_AZADApplications = Get-AzureADApplication -all:$true | sort-object displayname

#This goes through all the Service Principals and identifies any corresponding App Registrations
ForEach($AZADServicePrincipal in $ALL_AZADServicePrincipals){
    
    #Checks for expiration
    $SPexpiration = ($AZADServicePrincipal | Select-Object -ExpandProperty passwordcredentials | Select-Object -ExpandProperty enddate | Sort-Object) -join ";"
    If($SPexpiration -notlike "*/*"){$SPexpiration = $null}

    #Checks for a corresponding App Registration
    $AZADApplication = $null
    $AZADApplication = $ALL_AZADApplications | Where-Object appid -eq $AZADServicePrincipal.appid

    If($AZADApplication -ne $null){
        $ADDisplayname = $AZADApplication.displayname
        $ADObjectID = $AZADApplication.objectID
        $ADAppID = $AZADApplication.AppID

     #Checks for client secret expiration on the corresponding App Registration
     $ADClientSecretExpiration = $null     
     $ADClientSecretExpiration = ($AZADApplication | Select-Object -ExpandProperty passwordcredentials | Select-Object -ExpandProperty enddate | Sort-Object) -join ";"     
     If($ADClientSecretExpiration -notlike "*/*"){$ADClientSecretExpiration = $null}

     #Checks for certificate expiration on the corresponding App Registration
     $ADCertExpiration = $null
     $ADCertexpiration = ($AZADApplication | Select-Object -ExpandProperty KeyCredentials | Select-Object -ExpandProperty enddate | Sort-Object) -join ";"
     If($ADCertexpiration -notlike "*/*"){$ADCertexpiration = $null}
    }Else{
        $ADDisplayname = $null
        $ADObjectID = $null
        $ADAppID = $null
        $ADClientSecretExpiration = $null
        $ADCertExpiration = $null
    }

    #Calls the function to build the array
    AppArray -SPDisplayname $AZADServicePrincipal.displayname -SPExpiration $SPexpiration -SPObjectID $AZADServicePrincipal.ObjectId -SPAppID $AZADServicePrincipal.AppId -ADDisplayname $ADDisplayname -ADObjectID $ADObjectID -ADAppID $ADAppID -ADClientSecretExpiration $ADClientSecretExpiration -ADCertExpiration $ADCertExpiration
}

#This finds any app registrations that don't have a corresponding Service Principal and adds them to the $AllApps array
ForEach($AZADApplication in $ALL_AZADApplications){
    If($AllApps | Where-Object spappid -eq $AZADApplication.appid){}Else{

     #Checks for client secret expiration
     $ADClientSecretExpiration = $null
     $ADClientSecretExpiration = ($AZADApplication | Select-Object -ExpandProperty passwordcredentials | Select-Object -ExpandProperty enddate | Sort-Object) -join ";"
     If($ADClientSecretExpiration -notlike "*/*"){$ADClientSecretExpiration = $null}

     #Checks for certificate expiration
     $ADCertExpiration = $null
     $ADCertexpiration = ($AZADApplication | Select-Object -ExpandProperty KeyCredentials | Select-Object -ExpandProperty enddate | Sort-Object) -join ";"
     If($ADCertexpiration -notlike "*/*"){$ADCertexpiration = $null} 

        #Calls the function to build the array
        AppArray -SPDisplayname $null -SPExpiration $null -SPObjectID $null -SPAppID $null -ADDisplayname $AZADApplication.DisplayName -ADObjectID $AZADApplication.ObjectID -ADAppID $AZADApplication.AppID -ADClientSecretExpiration $ADClientSecretExpiration -ADCertExpiration $ADCertExpiration
    }
}

#Export the results
$date = Get-Date -Format yyyyMMdd
$AllApps| Where-Object {($_.spexpiration -ne $null) -or ($_.ADClientSecretExpiration -ne $null) -or ($_.ADCertExpiration -ne $null)} | Export-Csv -Path "<path>\<filename> - $date.csv" -NoTypeInformation

26 thoughts on “Find Expiring Enterprise Applications and App Registrations

    1. You probably need to check and see if the earlier variables have data. Start with these two and see if they contain anything:

      $ALL_AZADServicePrincipals
      $ALL_AZADApplications

        1. If you run the following, I’d expect the output to return a list of dates. What do you see?

          Connect-AzureAD
          get-azureadserviceprincipal -all $true | select-object -expandproperty passwordcredentials | select-object -expandproperty enddate

          1. Hi Vince, I do get a list of dates when I run that command. Any other ideas why the script is returning no results?

          2. A bit more digging…When I check the $AllApps variable, I get a list of every app, but none of them have an SPexpiration or an ADexpiration listed.

          3. Ok, I believe I discovered the problem. On line 4, I had somehow forgotten a few variables. Line 4 should read like this

            Function AppArray($SPDisplayname,$SPExpiration,$SPObjectID,$SPAppID,$ADDisplayname,$ADObjectID,$ADAppID,$ADexpiration){

            I had left out $SPADIntegrated,$SPExpiration despite showing it in the example section in blue. I’m not sure how I managed to do that. Update line 4 and see if that helps. If that does the trick, please let me know and I’ll update the article.

          4. Still the same result. I also went back and copied each section of powershell script from your examples and created a new ps script from those. Same result with that, as well. I would assume you are getting the same result when running against your tenant?

          5. Alright, I’ve gone through and completely updated the whole thing. The script I had posted here was a trimmed down version of what I use for work and apparently along the way I had accidentally messed up a few spots. I’ve tested the code I have at the bottom of the page now and it works (assuming you update the path the export-csv command is pointing to). You may need to use CTRL+F5 to force the page to reload entirely. Note to self: proofread

  1. Hello, Thanks for the script. However, SPExpiration is not getting populated and for some entries ADExpiration is null too. I know for sure from the portal, that some Apps which are showing null does have a expiry.

    1. The expirations that you see, are they Client Secrets or are they Certificates? I’ve updated the script on my end to include Certificates (for app registrations). The script posted here only grabs the Client Secrets info currently.

    1. At the time I wrote this script, I found no way of pulling the Notes field. This was something I was wanting to do as well, but Microsoft informed me that there was no way to get this field with PowerShell. It’s possible this has changed since I last bothered to try, but I highly doubt it.

      1. Jigar, sorry for the delay but I’ve updated the script on the page. The main difference is instead of the variable $ADExpiration there are now two variables: $ADClientSecretExpiration and $ADCertExpiration.

          1. Really appreciate you looking into this. Also the SP Expiration is not left aligned. I mean I’m trying to automate based on the CSV. Hence just wanted to see if there is a easy fix?

  2. Hi Vince,

    I keep getting this error. I think the article isn’t amended since, people were coming across same issue. Could you please help to amend the script. I keep getting below error.

    Get-AzADServicePrincipal : A parameter cannot be found that matches parameter name ‘all’.
    At line:24 char:55
    + $ALL_AZADServicePrincipals = Get-AzADServiceprincipal -all $true | so …
    + ~~~~
    + CategoryInfo : InvalidArgument: (:) [Get-AzADServicePrincipal], ParameterBindingException
    + FullyQualifiedErrorId : NamedParameterNotFound,Get-AzADServicePrincipal

    Get-AzADApplication : A parameter cannot be found that matches parameter name ‘all’.
    At line:25 char:45
    + $ALL_AZADApplications = Get-AzADApplication -all $true | sort-object …
    + ~~~~
    + CategoryInfo : InvalidArgument: (:) [Get-AzADApplication], ParameterBindingException
    + FullyQualifiedErrorId : NamedParameterNotFound,Get-AzADApplication

    Select-Object : Property “enddate” cannot be found.

    1. I realize you left this comment in January and I’m just now seeing it. It looks like you’re missing the colon

      Get-AzADServicePrincipal -all:$true

  3. Thanks for this great script 🙂
    One thing I’m missing is the expiration date of the app proxy certificate. Is there a possibility to add this? Would be really great.

Leave a Reply to Chris Cancel reply

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