Azure AD Application Registration inventory script

I’ve covered the topic of inventorying Azure AD integrated applications multiple times already, most recently in this article, where we switched to using the Graph API directly. The script remains relevant, as Microsoft is yet to cover application permissions in their OAuth inventory (part of Defender for Cloud apps/MCAS), and only offer this as part of the new App governance SKU, and add-on to the already expensive MCAS.

Another similar scenario that you’d likely want to address is to inventory all the first-party applications, or the app registrations for your tenant. Those are represented by an application object within Azure AD, and have most of the same properties as the service principal objects. There are however few differences, and important ones at that. As you “own” the application object, you are in charge of defining permissions (AppRoles and Scopes) as well as API integrations for it. And, you are also in charge of managing the credentials for the application. Last but not least, you can have the application verified or certified by Microsoft. Those are all details that the new, Azure AD app registration inventory script covers. Let’s dig into the details a bit.

First and foremost, the script will again use Graph API calls directly, which in turn means that you have to take care of authentication. The example I’ve used within the script leverages application permissions (client secret) and expects you to have an app prepared, with Directory.Read.All permissions granted. Make sure to fill in the relevant details on lines 67-79, or feel free to replace the whole connectivity block with your own preferred method.

Once authentication has been taken care of and valid token is obtained, the script will proceed with enumerating all the application objects within the organization. It will then iterate over each application to prepare the output object. As simple as that really, a single call against the Graph API is all we need, the rest is just “cosmetics”. Well, to present you with “human readable” values for the permissions (“Roles” and “Scopes”) required by the application, we will have to make few additional calls, one per each “resource”. This is utilized by the parse-AppPermissions helper function, which breaks down the permission entries into “application” and “delegate” ones, and prepares a concatenated string of each. In turn, the Get-ServicePrincipalRoleById function “translates” those pesky GUIDs into values such as “User.Read”.

Lastly, we utilize the parse-Credential function, which takes a look at the key/password credential objects for each application and provides some additional info. Don’t worry, the script does not access the actual values here, they’re not exposed (still, it goes without saying that you should not be blindly running scripts you downloaded from the internet!). It will however report on the number of such credentials configured per app, whether any of them has expired already, and whether their validity is set to over 1 year (365 days, if you want to change the value edit line 55). Those are all good practices to follow in securing access to your applications, and part of the “zero trust” approach.

And that’s pretty much all there is to it, the rest is just handling the output and exporting it to a CSV file. I’d recommend using Excel’s conditional filtering functionality to then highlight entries that need attention, such as those using client secret instead of certificate, expired credentials, those requiring excessive permissions, using deprecated permissions, and so on. Here’s an example:

You can find the script over at my GitHub repo. As usual, comments and suggestions are welcome.

Posted in Azure AD, Graph API, Microsoft 365, Office 365 | Leave a comment

Access Azure AD sign-in events for service principals via the Graph API

A while back, Microsoft released one of the most important feature updates, finally allowing us to monitor service principal sign-ins. At that time, the sign-in logs were only available as part of the Azure AD blade, but there were hints that Microsoft will make them available via other endpoints too. Now, we get to explore them via the Graph API!

If you are already using the /auditLogs endpoint, chances are you will need minimal code changes in order to start collecting service principal sign-ins. In a nutshell, all you need to do is add a filter on the signInEventTypes property. The available values for said property are: interactiveUser, nonInteractiveUser, servicePrincipal, managedIdentity, and unknownFutureValue. For this scenario, we are interested in the “servicePrincipal” value. Do note that the use of the $filter operator seems to require the /beta endpoint currently, and only works with an “eq” statement. Without further ado, here’s how a query that filters only service principal sign-in events looks like:

GET$top=1&$filter=createdDateTime ge 2022-01-10T06:00:00Z and createdDateTime le 2022-01-17T06:00:00Z and signInEventTypes/any(t:t eq 'servicePrincipal')

The request itself requires both Directory.Read.All and AuditLog.Read.All permissions, and if you are using the Delegate permissions model, the user needs to also be assigned a role with sufficient permissions to access the Azure AD sign-in logs. If those requirements are met, you can query the same endpoint via the Graph explorer tool:

The screenshot above is trimmed, as you can expect the full set of details/properties corresponding to the sign-in event to be returned, and there are a lot of these. Which is a good thing, don’t get me wrong – it allows you to fetch all the relevant data in a single query. We can then compare the Graph API entry against the Azure AD blade entry, which is easily done by filtering on the correlationId value:

Don’t forget that entries in the Azure AD blade UI are aggregated together, so you might need to adjust the aggregation window (shouldn’t matter when filtering for specific correlationId though).

One thing to keep in mind is that currently, service principal sign-in events are not returned by default. You have to specifically request them, by adding the “signInEventTypes/any(t:t eq ‘nonInteractiveUser’)” filter statement. In addition, the endpoint does not currently support the $select operator, meaning you cannot limit the output to just select properties. You can of course do so client-side.

For the sake of completeness, here’s also sample PowerShell-based code to query service principal events. Make sure to obtain a valid access token with sufficient permissions first, and pass it as part of the $authHeader1 variable.

$GraphSP = @()
$uri = "`$top=100&`$filter=createdDateTime ge 2022-01-15T06:00:00Z and createdDateTime le 2022-01-17T06:00:00Z and signInEventTypes/any(t:t eq 'servicePrincipal')"

do {
$result = Invoke-WebRequest -Uri $uri -Verbose:$VerbosePreference -ErrorAction Stop -Headers $authHeader1
$uri = ($result.Content | ConvertFrom-Json).'@odata.nextLink'
#If we are getting multiple pages, best add some delay to avoid throttling
Start-Sleep -Milliseconds 500
$GraphSP += ($result.Content | ConvertFrom-Json).Value
} while ($uri)

Once you obtain the sign-in entries, you can manipulate them as any other PowerShell object:

PS C:\> $GraphSP[0].appId

PS C:\> $GraphSP[0].status

errorCode failureReason additionalDetails
--------- ------------- -----------------
0 Other.

And in case you were wondering whether the Management activities API, the Audit UI in the Compliance center or the Search-UnifiedAuditLog cmdlet expose such events, the answer seems to be NO. The Graph API method outlined above or the Azure AD blade are your options, as well as exporting to a event hub, etc.

Posted in Azure AD, Graph API, Microsoft 365, Office 365 | Leave a comment

Quickly list all Groups and Teams a user is member of in Microsoft 365

In one of my most popular articles, I discussed several tips and tricks on how to quickly fetch group membership for a given user. As the article was written almost 5 years ago, some of the examples therein are now outdated, and some might even stop working due to upcoming deprecations. So in this article, I will provide you with updated examples, using the latest and greatest available methods at the time of writing.

The Exchange Online cmdlets remain the easiest method to fetch group membership across any and all group types supported by Exchange. Thanks to the use of server-side filtering, the output is trimmed down to just the objects we care about, and is usually fast enough to warrant the use of the “old-style” Get-Recipient cmdlet:

Get-Recipient -Filter "Members -eq 'CN=user,,OU=Microsoft Exchange Hosted Organizations,DC=EURPR03A001,DC=prod,DC=outlook,DC=com'"

where you need to provide the DistinguishedName value for the user you want to fetch groups for. An easier example combines the output of the Get-Mailbox cmdlet to fetch the DN value first, and then uses it for the server-side filter:

$dn = (Get-Mailbox

Get-Recipient -Filter "Members -eq '$dn'"

And just in case you have some immensely overcrowded environment, here’s how you can perform the same action by leveraging the REST-based Exchange Online cmdlets, part of the V2 module:

Get-EXORecipient -Filter "Members -eq 'CN=user,,OU=Microsoft Exchange Hosted Organizations,DC=EURPR03A001,DC=prod,DC=outlook,DC=com'"

or by fetching the DN first:

$dn = (Get-EXOMailbox

Get-EXORecipient -Filter "Members -eq '$dn'"

We no longer need to use the –RecipientTypeDetails parameter to specifically request Office 365/Microsoft 365 Groups, as those are now included in the output by default. Thus the cmdlets above will cover all Distribution groups, Mail-enabled security groups and Microsoft 365 Groups the user is member of. No Azure AD security groups are included though!

To cover any Dynamic distribution groups, which are not included in any of the above examples, we can leverage the recently introduced Get-DynamicDistributionGroupMember cmdlet. Unfortunately, the cmdlet does not support server-side filters, so we need to do things the stupid way. The example below should return any Dynamic distribution groups a given user is a member of:

$dn = (Get-Mailbox

Get-DynamicDistributionGroup | ? {(Get-DynamicDistributionGroupMember -Identity $_.PrimarySMTPAddress | ? {$_.DistinguishedName -eq $dn})}


In the old article, we also covered some examples on how to perform similar operations via the Azure AD PowerShell module. As said module is going to be deprecated soon, we can instead use the Microsoft Graph SDK (PowerShell module), or if you prefer, call the Graph API endpoints it leverages directly. Here are few examples on that.

First, to query all group objects a given user is a member of, we can use the /memberOf endpoint. Since the output will also include admin roles the user has been assigned to, we can further narrow it down by adding an additional filter. Lastly, we can also request the count of objects to be added to the output, although this will also require a special header to be added to the request (“consistencyLevel”=”eventual”):




To perform the same queries via the Microsoft Graph SDK, use the following cmdlets:

Get-MgUserMemberOf -UserId

Get-MgUserMemberOf -UserId | ? {$_.AdditionalProperties['@odata.type'] -ne '#microsoft.graph.directoryRole'}

You will of course notice that the output of the MG PowerShell cmdlets is not as easy to work as the one from the Exchange cmdlets we used above, but that’s what we get with auto-generated modules…

The Graph API, and to an extent the MG PowerShell cmdlets also allow us to query “transitive” group membership, as detailed for example here. This in turn allows us to easily include any “nested” groups a given user is a member of in the output. Or to put it another way, we can “flatten” the membership list. Here are some examples:




And the same via the Microsoft Graph SDK:

Get-MgUserTransitiveMemberOf -UserId

Get-MgUserTransitiveMemberOf -UserId | ? {$_.AdditionalProperties['@odata.type'] -ne '#microsoft.graph.directoryRole'} 


Last, let’s also examine some methods to directly fetch a list of all Teams a given user is a member of. The methods outlined above all include this data, so technically we can filter the output to just Teams object, but that’s not always a straightforward task. Instead, we can leverage the unified Group cmdlets, Get-UnifiedGroup and Get-UnifiedGroupLinks:

Get-UnifiedGroup -Filter {ResourceProvisioningOptions -eq "Team"} | ? {(Get-UnifiedGroupLinks $_.Guid.Guid -LinkType member | ? {$_.PrimarySmtpAddress -eq ""})}

Here’s how to do the same via the MicrosoftTeams module:

Get-Team | ? {Get-TeamUser -GroupId $_.GroupId | ? {$_.User -eq ""}}

The Graph API allows us to use the /joinedTeams endpoint:


Finally, via the Get-MgUserJoinedTeam cmdlet:

Get-MgUserJoinedTeam -UserId
Posted in Azure AD, Exchange Online, Graph API, Microsoft 365, Microsoft Teams, Office 365, PowerShell | Leave a comment

When was my Microsoft 365 tenant created?

The question of “when was my organization’s tenant created” popped up recently, so I thought I’d address it in a short blog post. After all, it’s an interesting bit of information to have, and as it turns out it’s now actually displayed on the landing page of the Teams Admin Center. This in fact is the easy answer here, if you are only interested in the value and not the means to obtain it, you can easily do so from the TAC, assuming you have access to it.

OK, but what if you don’t have admin privileges? Or if you want to obtain this information programmatically? The answer is again simple – use the Graph API and specifically, the /organization endpoint. The below screenshot illustrates the information you can obtain from said endpoint, which includes the createdDateTime property:



So this particular tenant (one of my DEV tenants) has been created back in April 2013. Do note that the value of the createdDateTime property will only be shown if you have the necessary permissions, such as Organization.Read.All or Directory.Read.All. Calling this endpoint without the required permissions will still return some data, however most of the fields, including createdDateTime, will show null values. The screenshot below illustrates how this looks like – same query, same tenant, different user with insufficient permissions – null value for the createdDateTime property.

In effect, for a regular user account with no additional permissions within the Office 365 tenant, there is no way to obtain this information. What about PowerShell, one might ask? Well, while most organizations do not block PowerShell access, and regular users can still connect and use cmdlets such as Get-MsolCompanyInformation, said property is not actually exposed therein. The Azure AD module won’t help you either, and the Microsoft Graph SDK has the same permission requirements as the direct Graph API calls above.

What you can do however is look at user’s properties, via the Get-MsolUser cmdlet or similar. Therein, look for the value of the WhenCreated property, and choose the oldest one. Or, if you happen to know what the initial admin user is, you can check his properties directly. I won’t teach you how to do any of these things, as they can be used for harvesting directory data, but suffice to say it’s doable, and can give you a rough approximation of the tenant’s creation date. Still, it’s not the same thing as user accounts come and go.

Alternatively, you can look into the properties of some other objects within the directory. For example, the output of Get-OrganizationConfig. Or the properties of the default accepted domain, via Get-AcceptedDomain, the default OU, and a whole array of other Exchange-specific containers. This will only work if the tenant has been enabled for Exchange Online, obviously, and the value will only be relevant if the Exchange workload was provisioned from the get go.

Get-AcceptedDomain | sort WhenCreatedUTC | select WhenCreatedUTC -First 1

Get-OrganizationalUnit | sort WhenCreatedUTC | select WhenCreatedUTC -First 1

Get-OrganizationConfig | select WhenCreated

18/04/2013 09:03:15

You get the idea. Similar tricks might be applied to other workloads, however you need to keep in mind that not all of them expose properties such as WhenCreated, and even if they do, the workloads themselves might be quite “younger” compared to the age of your tenant. Remember that back when Office 365 launched in 2011, only a handful of the services we’re using today were available!

Posted in Azure AD, Graph API, Microsoft 365, Office 365 | Leave a comment

Updated version of the Reporting on Teams apps and tabs script

Two years back, I published a short “proof of concept” script that helps you enumerate Teams apps and tabs across all your organization’s Teams. Due to some recent changes in the Graph API endpoints required to use the script, it’s time for an updated version.

Probably the most important change that has happened since the original script was released is around permissions. While previously it was possible to leverage “wide” permissions such as Group.Read.All, Group.ReadWrite.All, Directory.Read.All, or Directory.ReadWrite.All, these are no longer supported for the relevant Graph API endpoints. Instead, we should use “narrower” scopes such as TeamsAppInstallation.ReadForTeam.All and TeamsTab.Read.All. While this change doesn’t directly affect the script itself, you need to make sure the corresponding Azure AD application has been updated with the relevant scopes.

Still, I took the opportunity to rework the authentication part a bit, and it should be much cleaner now. I’d still recommend plugging in your preferred “get token” function instead, especially if you are working for a larger organizations with gazillion teams. At the very least, make sure to add some proper error handling and token renewal logic.

Another small change that was made is to leverage the LIST method for the /teams endpoint, which is now available in beta. So we no longer need to query Groups instead. While the current version of said method has some limitations, for the purposes of our script we only need the team ID and name, so it will do just fine.

The last change made to the script is to add some additional properties to the output, thanks to the availability of the teamsAppDefinition resource, which we can now fetch as part of the single query we run against the /installedApps endpoint, per team. Such properties include the Version of the app, the ID of the Azure AD Application used by it, if any, the Description, and available installation scopes. On the Tabs front, information about the date the tab was added will be presented in some case. Unfortunately, many of these fields will return null values for various apps, which remains an issue that Microsoft needs to resolve. As is the total lack of visibility in terms of who the Publisher (vendor) of a given app is, the permissions required, certifications and so on – all information readiliy available from other endpoints, but not the Graph API.

Last thing I’d want to mention is that you will likely notice an increased number of entries returned in the output. Microsoft has not been idling and a bunch more “default” apps are installed across teams now, such as the Breakout rooms one, or Share to Teams, etc. On the other side of the spectrum, many apps have been marked as “legacy”, but their entries are still returned in the output.

And that’s it, I suppose. You can find the updated version over at my GitHub repo. Let me know if you run into any issues.

Posted in Graph API, Microsoft 365, Microsoft Teams, Office 365, PowerShell | Leave a comment