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 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


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


}Else{


$ADDisplayname = $null
$ADObjectID = $null
$ADAppID = $null
$ADexpiration = $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 -SPSSOApplication $sso -SPADIntegrated $adintegrated -SPExpiration $SPexpiration -SPObjectID $AZADServicePrincipal.ObjectId -SPAppID $AZADServicePrincipal.AppId -ADDisplayname $ADDisplayname -ADObjectID $ADObjectID -ADAppID $ADAppID -ADexpiration $ADexpiration
} #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,$SPSSOApplication,$SPADIntegrated,$SPExpiration,$SPObjectID,$SPAppID,$ADDisplayname,$ADObjectID,$ADAppID,$ADexpiration){
$global:AllApps += @(
$AppObject = New-Object -TypeName psobject
$AppObject | Add-Member -MemberType NoteProperty -Name SPDisplayname -Value $SPDisplayname
$AppObject | Add-Member -MemberType NoteProperty -Name SPSSOApplication -Value $SPSSOApplication
$AppObject | Add-Member -MemberType NoteProperty -Name SPADIntegrated -Value $SPADIntegrated
$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 ADExpiration -Value $ADexpiration
$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 spappid -eq $AZADApplication.appid){}Else{
$ADexpiration = ($AZADApplication | Select-Object -ExpandProperty passwordcredentials | Select-Object -ExpandProperty enddate | Sort-Object) -join ";"
If($ADexpiration -notlike "/"){$ADexpiration = $null}
AppArray -SPDisplayname $null -SPSSOApplication $null -SPADIntegrated $null -SPExpiration $null -SPObjectID $null -SPAppID $null -ADDisplayname $AZADApplication.DisplayName -ADObjectID $AZADApplication.ObjectID -ADAppID $AZADApplication.AppID -ADexpiration $ADexpiration
}
}

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 {($_.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,$SPSSOApplication,$SPObjectID,$SPAppID,$ADDisplayname,$ADObjectID,$ADAppID,$ADexpiration){
     $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 ADExpiration -Value $ADexpiration
         $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 appid -eq $AZADServicePrincipal.appid

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

         #Checks for expiration on the corresponding App Registration
         $ADexpiration = ($AZADApplication | Select-Object -ExpandProperty passwordcredentials | Select-Object -ExpandProperty enddate | Sort-Object) -join ";"
         If($ADexpiration -notlike "/"){$ADexpiration = $null}
     }Else{
         $ADDisplayname = $null
         $ADObjectID = $null
         $ADAppID = $null
         $ADexpiration = $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 -ADexpiration $ADexpiration
 }

#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 spappid -eq $AZADApplication.appid){}Else{

         #Checks for expiration
         $ADexpiration = ($AZADApplication | Select-Object -ExpandProperty passwordcredentials | Select-Object -ExpandProperty enddate | Sort-Object) -join ";"
         If($ADexpiration -notlike "/"){$ADexpiration = $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 -ADexpiration $ADexpiration
     }
 }

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

Leave a Reply

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