STOP RIGHT THERE. I have a newer fancier post covering this topic right here. However, if you’d like to continue using my legacy method, feel free to continue reading
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’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.
#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
Hello,
This script is returning no results and I cannot figure out why. Can you assist?
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
Hi Vince,
Thanks for the reply. I checked all of the variables and they do return the proper data.
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
Hi Vince, I do get a list of dates when I run that command. Any other ideas why the script is returning no results?
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.
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.
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?
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
Success! Excellent work and beautiful script. Thanks for resolving the issues with it!
☜(゚ヮ゚☜)
Yes, I get the expected (long) list of dates.
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.
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.
Hello, is it possible to get corresponding owner and information from Notes field?
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.
Can I get the script for app registrations which can include client secrets and certificates?
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.
Thanks. This is helpful. Any idea to get the Notes field?
Thanks. It is creating duplicate rows. Any ideas?
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?
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.
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
Very nice, thank you.
Actually Microsoft have recently published this script related to this topic: https://learn.microsoft.com/en-us/azure/active-directory/manage-apps/scripts/powershell-export-apps-with-expriring-secrets
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.
Super. very good job.
Thanks a Lot. You have pointed out the exact issue which I was facing with App Registration and Service Principal. All those available scripts were giving me either of the result but not the intended one. Great Job Man, appriciate your efforts and knowledge. Thanks a ton. God bless you.
Appreciate the kind words. I still use this script (or a version of it) regularly.
Unfortunately when I run this the results do not export at the end. I get no errors when running the script. Just no results when it completes.
Still — even with that list, I’m back to the problem of having a whole bunch of different certificates to manually renew throughout the year. That’s a problem I was having with purchasing SSL certificates, if there’s 15 websites, you constantly have to update them throughout the year! Considering switching to Let’s Encrypt just to avoid that; but here we’re back to the same problem. Is there any way to automate the process? Or just set a client secret that expires in a million years.
Hi VINCE, first of all, I need to say thank you for sharing this info, is SUPER amazing, I would like to know if you have plans to update it to MS graph?
Regards,
Ernesto
Pura Vida!
I do actually have a much better version of this re-written using graph, but have yet to actually post it. I’ll hopefully get around to doing so in the near future!
That sounds really great.
I hope you find time for it soon 🙂
Good news, here you go https://www.vincecarbone.com/2024/09/04/find-expiring-enterprise-applications-and-app-registrations-with-microsoft-graph/
Finally got around to posting it https://www.vincecarbone.com/2024/09/04/find-expiring-enterprise-applications-and-app-registrations-with-microsoft-graph/
Hi Vince,
This is what I was looking for – many thanks and if you get some time to update this script with MS-Graph that would be great.
Cheers!!!
Rikin
new and improved microsoft graph version https://www.vincecarbone.com/2024/09/04/find-expiring-enterprise-applications-and-app-registrations-with-microsoft-graph/
Date format is different for all secret, can you provide the script for the same date format for all certificate and secret