How to Upgrade Office 365 PowerShell Scripts to Use the Graph API

Proving the Graph is Quicker

Without a doubt, using Graph API calls is much faster to retrieve Office 365 than PowerShell cmdlets are. It’s more obvious in complex scripts like the Groups and Teams activity script, which is much faster in its Graph variant (5.1) than its counterpart (4.8) due to Graph API calls replacing cmdlets from the Exchange Online and SharePoint Online modules. Speed matters, especially when a tenant supports thousands of groups, and the Graph API version of the script can process large quantities of groups where the pure PowerShell version struggles to cope.

Seeking an Example to Convert

Good as it is to have a speedier script, the complexity of the activity report is possibly not a good test case to illustrate the decision process that you should go through to decide if it’s a good idea to upgrade a script to use the Graph API, and then measure the improvement. Simplicity is better, so let’s explore what needs to be done to upgrade the Microsoft 365 Groups Membership report script. This script uses the following cmdlets:

  • Get-AzureADUser: Fetch the list of Azure AD accounts in the tenant. Microsoft announced their intention to retire the Azure AD module in June 2021, so this is a good example of the kind of script update needed to replace Azure AD calls with Graph API calls.
  • Get-UnifiedGroup: Fetch the list of Teams in the tenant.
  • Get-Recipient: Fetch the list of Groups a user account belongs to.

Two cmdlets are from the Exchange Online Management module, one is from the Azure AD module. A bunch of other processing is done to filter, sort, and process the data fetched by these cmdlets, but essentially the conversation involves replacing these cmdlets with Graph API calls. Sounds easy.

Using the Graph API

Before any script can use the Graph API, it needs to use an Azure AD registered app. The app serves as the holder for permissions to allow the script to access data. Every registered app has a unique identifier. When you create a registered app, you can generate an app secret. When running the app, we need to know the secret to prove we have permission to use the app.

Before making any calls, you need to know your tenant identifier. This is the GUID for a tenant as returned by the Get-AzureADTenantDetail cmdlet. The Connect-MicrosoftTeams cmdlet also returns this information. Bringing everything together, before we can use an app to access Graph APIs, we need to know the app identifier, app secret, and tenant identifier. You’ll often see code like this in scripts:

$AppId = "a09cf913-5ff9-48a2-8015-f28f2854df26"
$AppSecret = "u6X7_i8K-yhh-b4-z5FEmj_wH_M~nIOz4n"
$TenantId = "22e90715-3da6-4a78-9ec6-b3282389492b"

It’s an important part of working with Graph API apps to understand how permissions work and to ensure that apps receive only the permissions necessary to work with the data they process. In the case of our app, we need:

  • Group.Read.All: to read information about Microsoft 365 Groups in the tenant.
  • GroupMember.Read.All: to read information about the membership of Microsoft 365 Groups.
  • User.Read.All: to read information about Azure AD accounts in the tenant.

Like all programming tasks, it soon becomes second nature to assign the correct permissions, sometimes after browsing Microsoft’s documentation to find the correct permission.

An administrator gives consent to allow the app to use its assigned permissions. Hackers can use OAuth consents as a method to gain permissions to access data, so it’s wise to keep an eye on consents given within an organization, with or without Microsoft’s new App governance add-on for MCAS.

Access Token

Equipped with a suitably permissioned app, app secret, and tenant identifier, the app can request an access token by posting a request to the token endpoint. For Graph API calls to Office 365 data, that’s going to be something like:

https://login.microsoftonline.com/tenant-identifier/oauth2/v2.0/token

The bearer token issued in response confirms that the app has the necessary permissions to access data like Users, Groups, and Sites and whether access is read-only or read-write. The token is included in the authorization header of the requests made to the Graph APIs. Access tokens expire after an hour, so long-running programs need to renew their token to continue processing.

All of this sounds complicated, but once you do it for one script, it becomes second nature to acquire an access token and be ready to start using Graph API calls.

Replacing PowerShell Cmdlets

Like anything else, it takes a little while to become used to fetching data using Graph API calls. You must pay attention to the data that’s fetched. First, to limit the demand on resources, the Graph fetches limited data at one time and you must iterate until no more data is available (a process called pagination). Second, in some cases, the property names used by cmdlets vary to what’s used by the Graph API. For example, the Get-UnifiedGroup cmdlet returns the description of a group in the Notes property whereas the Groups API uses description.

The Graph Explorer is an online Microsoft tool to help developers become accustomed to Graph API syntax and data. You should use the Explorer to test calls before including them in a script. Debugging calls using the Graph Explorer saves a lot of time and heartache. Figure 1 shows the Graph Explorer being used to examine the transitive set of groups returned for a user.

Using the Graph Explorer to test Graph API Calls
Figure 1: Using the Graph Explorer to test Graph API Calls

The Graph Explorer is a Graph app. Like any other app, it needs permissions to access data. If a call fails, the Explorer tells you which permissions are missing and you can then consent to the assignment.

Fetching Azure AD Accounts

The first cmdlet we need to replace in the script is the one to fetch the list of Azure AD users in the tenant. In PowerShell, this is:

$Users = Get-AzureADUser -All:$true

The Graph Users API fetches the same data. Unlike the Get-AzureADUser cmdlet, a default set of properties is returned and we must be specific if we want other properties. Here’s the call used.

$Uri = https://graph.microsoft.com/v1.0/users?&`$select=displayName,usertype,assignedlicenses,id,mail,userprincipalname
$Users = Get-GraphData -AccessToken $Token -Uri $Uri

Get-GraphData is a wrapper function to take care of pagination and extraction of data returned by Graph API calls in a form like the PowerShell objects returned by cmdlets. You can make your life easier by including a function like this in any script which interacts with Graph API calls. To see an example of the function I use, download the Graph version of the group membership report script from GitHub.

Fetching Teams

The second call fetches the list of team-enabled groups. The script then creates a hash table to store the teams so that it can be used as a lookup to see if a group is team-enabled when reporting its properties. The PowerShell code uses a server-side filter with the Get-UnifiedGroup cmdlet to return the teams.

$Teams = Get-UnifiedGroup -Filter {ResourceProvisioningOptions -eq "Team"} -ResultSize Unlimited | Select ExternalDirectoryObjectId, DisplayName

The Graph Groups API equivalent uses the same kind of filter:

$Uri = "https://graph.microsoft.com/beta/groups?`$filter=resourceProvisioningOptions/Any(x:x eq 'Team')"
$Teams = Get-GraphData -AccessToken $Token -Uri $Uri

Fetching the Groups a User Belongs to

The last call we make is to find the set of groups a user account is a member of. The Get-Recipient cmdlet is very fast at returning the list of groups based on the distinguished name of an account. The user data returned by Get-AzureADUser doesn’t give us the distinguished name (it is an Exchange Online property), so we must run Get-Recipient twice: once to get the distinguished name, and then use the distinguished name to find the groups.

$DN = (Get-Recipient -Identity $User.UserPrincipalName).DistinguishedName
$Groups = (Get-Recipient -ResultSize Unlimited -RecipientTypeDetails GroupMailbox -Filter "Members -eq '$DN'" | Select DisplayName, Notes, ExternalDirectoryObjectId, ManagedBy, PrimarySmtpAddress)

The Graph Users API can resolve a transitive lookup against groups to find membership information for an account. We can therefor use a call like this:

$Uri = "https://graph.microsoft.com/v1.0/users/" + $user.id +"/transitiveMemberOf"
$Groups = Get-GraphData -AccessToken $Token -Uri $Uri

That’s it. All the other command in the script process data fetched from Azure AD or Exchange Online. Apart from the changes detailed above, the same code is used for both the PowerShell and Graph versions of the script.

The Result

Your mileage may vary depending on the backend server you connect to, the state of load on the service, and other factors. My tests, which are surely as reliable as an EPA mileage figure, revealed that the PowerShell version processed accounts at a rate of about 0.6/second each. The Graph version reduced the time to about 0.4/second. In other words, a 50% improvement.

Not every script will benefit from such a speed boost and other scripts will need more work to install Graph turbocharging. But the point is that Graph-powered PowerShell is much faster at processing Office 365 data than pure PowerShell is. Keep that fact in mind the next time you consider how to approach building a PowerShell-based solution for Office 365.


Learn more about how Office 365 really works on an ongoing basis by subscribing to the Office 365 for IT Pros eBook. Our monthly updates keep subscribers informed about what’s important across the Office 365 ecosystem.

15 Replies to “How to Upgrade Office 365 PowerShell Scripts to Use the Graph API”

  1. probably a good suggestion to not store secrets in your scripts but call them from a secrets manager?

    1. Yes, but the exact way to handle that situation is very dependent on the decisions made in an organization. As I keep on saying, the code we write and post here is to illustrate a principle, not to be regarded as code ready to use in production. That’s why we don’t focus too much on error handling etc. because that’s another thing that’s highly organization-dependent.

    1. That module is still a work in progress. It’s basically a wrapper around the native Graph API calls. For now, I prefer to use Graph calls in my PowerShell scripts, but over time I acknowledge that the situation might change as the Microsoft Graph SDK for PowerShell (the official name for the module) matures and becomes more functional.

  2. You mention “Access tokens expire after an hour, so long-running programs need to renew their token to continue processing.” Do you have a function that would perform this operation?

  3. I must be stupid, but I am getting Forbidden (403) when I do this. I’ve admin-granted all the permissions mentioned. Using a standard or global admin account doesn’t change the error.

  4. Would it be possible to explain the purpose of x:x in the query “(x:x eq ‘Team’)” ? I have seen a:a as well in some other queries. Thank you.

    1. https://docs.microsoft.com/en-us/graph/query-parameters is the official documentation for search queries and https://docs.microsoft.com/en-us/graph/query-parameters covers filtering using any and all (lambda operators). I don’t see a good explanation for all the different types of queries used against the Graph, so I shall ask Microsoft if one exists. This also helps: https://stackoverflow.com/questions/49660791/azure-ad-graph-any-filter-syntax/49665624. Also this tutorial https://www.odata.org/getting-started/basic-tutorial/#lambda

  5. Thanks a lot for authoring an entire article just to explain lambda qualifiers. It helps a lot for non-developer admins to build PowerShell queries. Love your work, Chief. A Legend.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.