Get the Basics Right When Working with Graph APIs
Given the number of articles I write about developing PowerShell scripts based on the Microsoft Graph APIs or the Microsoft Graph PowerShell SDK, it’s inevitable that I receive many questions from people who encountered Graph API errors after downloading the scripts from my GitHub repository and trying to run the code in their tenant. In this article, I describe some of the common issues reported to me and potential solutions.
It’s PowerShell, So It’s Your Code
One of the great things about PowerShell is that once you download a copy of a script, it becomes your code. No secrets lurk in the code, and you have full control over changing it to meet your requirements. Great responsibility comes with great power, which means that you’re in charge of maintaining the code for your tenant. No one else has access to your Microsoft 365 data. No one else can amend the Graph permissions assigned to the apps used by PowerShell scripts.
Support is therefore your responsibility. Only you can tweak tenant settings or update Azure AD to address problems met when running a script. Treat every script you download as potentially dangerous until you complete a full end-to-end review of its code and understand exactly what the code does. Apart from anything else, this will identify the data accessed by the script and the permissions required in terms of Graph application permissions and Azure AD administrative roles.
A Correctly-Configured App is the Key to Avoiding Graph API Errors
PowerShell scripts can run Graph API requests in two ways:
- Using the cmdlets in the Microsoft Graph PowerShell SDK.
- Passing a URL to run a Graph request via the Invoke-RestMethod cmdlet.
In both cases, the Graph communicates with the script via an app registered in Azure AD. The app can be an enterprise app, meaning that it’s created and maintained by Microsoft or another party (like Adobe) and intended for use in multiple tenants, or it can be an app specific to your tenant. Both are Azure AD client applications that have service principals. We’ll get back to service principals soon.
The first step in communicating with the Graph for many PowerShell scripts is to register a new client application with Azure AD. The process is easy and results in two of the three pieces of information needed to authenticate with Azure and secure an access token. Those items are:
- The client (or application) identifier.
- The tenant (or directory) identifier.
Figure 1 shows details of a registered client application, and you can see the client identifier and tenant identifier. The last piece of information needed for authentication is some method to prove the identity of the app, or something known to the app that’s also known to Azure AD.
For testing purposes, scripts often use app secrets as proof of identity. It’s a bad idea to use app secrets in production (an exception might be if you store the app secrets somewhere safe, like Azure Key Vault). Production scripts more often use certificate-based authentication, where the app passes a certificate or thumbprint to Azure AD to prove its identity.
With the three pieces of information, an app can request Azure AD to issue an access token with code like this:
# Construct URI and body needed for authentication $uri = "https://login.microsoftonline.com/$tenantId/oauth2/v2.0/token" $body = @{ client_id = $AppId scope = "https://graph.microsoft.com/.default" client_secret = $AppSecret grant_type = "client_credentials" } # Get OAuth 2.0 Token $TokenRequest = Invoke-WebRequest -Method Post -Uri $uri -ContentType "application/x-www-form-urlencoded" -Body $body -UseBasicParsing # Unpack Access Token $Token = ($tokenRequest.Content | ConvertFrom-Json).access_token
An Azure AD access token contains details of the permissions the app can use. Securing an access token is the first step to successful use of Graph APIs by PowerShell scripts. An access token lasts approximately 59 minutes, after which it must be renewed to continue to be valid.
It’s Not Me, it’s the App!
Graph permissions are held by the app and not assigned to the user account used to sign into PowerShell. This is different from traditional PowerShell modules, where the administrative roles held by the user account dictate what cmdlets can do.
For instance, your account might hold the Exchange Online administrator role, but that doesn’t mean that you can run the mailbox content report script to retrieve information about what’s in a user mailbox. When the Graph assesses if an app can execute a request to fetch data, it does so on the basis of the permissions assigned to the app and nothing else.
Another critical aspect of Graph permissions is the difference between delegated permissions and application permissions. In a nutshell, delegated permissions allow an app to access data belonging to the signed-in account. Application permissions allow an app to access data from across the tenant, which is why an app must receive consent from an administrator before it can use application permissions. In most cases, the apps used by PowerShell scripts to perform administrative operations require application permissions.
Figure 2 shows an app assigned multiple Graph application permissions. In most cases, scripts don’t need such heavily-permissioned apps.
The majority of the problems I deal with when people try to run example scripts are due to the app missing a vital permission or using delegated permissions instead of application permissions.
Service Principals
Each app has a service principal. One way to think about a service principal is that it’s a convenient way for Azure AD to store permissions and roles for an app. When you run cmdlets from the Microsoft Graph PowerShell SDK in an interactive session, permissions come from the service principal for the Graph PowerShell app. This is an example of an enterprise app maintained by Microsoft. The service principal for the app holds the permissions available for SDK cmdlets in interactive sessions. Although convenient, the downside of this arrangement is that the service principal can become highly permissioned.
To illustrate the point, run the Get-MgContext cmdlet to report the available scopes (permissions) held by the SDK’s service principal:
(Get-MgContext).Scopes Analytics.Read Application.Read.All Application.ReadWrite.All AppRoleAssignment.ReadWrite.All AuditLog.Read.All Channel.ReadBasic.All ChannelMessage.Read.All ChannelMessage.Send ChannelSettings.Read.All Contacts.ReadWrite DelegatedPermissionGrant.ReadWrite.All Directory.AccessAsUser.All Directory.Read.All Directory.ReadWrite.All email Group.Read.All Group.ReadWrite.All GroupMember.Read.All GroupMember.ReadWrite.All IdentityProvider.ReadWrite.All Mail.Read Mail.ReadWrite Mail.Send
When you run SDK cmdlets in non-interactive mode, you use a registered client app that has its own set of permissions. The app can use certificate-based authentication, or the script can run in Azure Automation with a managed identity and use the service principal of the automation account to hold its permissions.
The Dreaded 403 Error
If a Graph request produces a 403 error, it means that the app is not authorized to make that request. In other words, it does not have the necessary application permission, and the Graph respectfully declines to allow access. If you see a 403 error, check the assigned permissions and make sure that an administrator consented to their use by the app.
In most cases, scripts include comments to tell administrators what permissions are needed. If necessary, you can check different sources to discover what permissions are needed to make a Graph request.
Microsoft’s documentation for Graph APIs always lists the required permissions. For instance, Figure 3 shows that the Graph List Groups API can be run if an app has one of several permissions, including Group.Read,All.
Microsoft’s documentation for Graph APIs has steadily improved recently and includes examples that can be plugged into the Graph Explorer to test requests. Many of the APIs now include PowerShell examples using SDK cmdlets. The PowerShell examples tend to be very basic but are enough to start. For instance, to fetch a list of groups, the List Groups API documentation suggests:
Get-MgGroup
The command works, but only fetches the first 100 groups in the tenant. A better example that fetches all groups is:
Get-MgGroup -All
Not Getting all the Expected Data
To limit potential strain on resources, Graph APIs return data in pages holding sets of items. Often, a page holds 100 items and a call to a Graph API will return those items and no more, even if more matching items are available. This mechanism is called pagination and it’s the responsibility of developers to check if more data is available and to continue fetching until they retrieve all the data.
Graph requests use the Top query parameter to fetch specific numbers of items at one time. However, the Top parameter is often limited to 999 items. A better solution is for scripts to check if the information returned by a Graph request includes a nextlink. A nextlink is a URL pointing to the next page of data to be fetched and the idea is that you continue fetching data until the Graph doesn’t return a nextlink. Often, scripts include a function to handle pagination (for an example, see the Get-GraphData function in the Teams and Groups Activity Report script).
Some SDK cmdlets take care of pagination by including support for an All parameter to force retrieval of all matching data. These cmdlets include Get-MgUser, Get-MgGroup, and Get-MgTeam (beta only). If in doubt, check the documentation!
Obfuscation
Microsoft 365 generates a ton of data about user activity that’s surfaced in the reports section of the Microsoft 365, SharePoint Online, and Teams admin centers. Applications retrieve usage data through the Graph Reports API. To preserve user privacy, Microsoft 365 implements a setting to obfuscate information by concealing display names and similar information when the Graph retrieves usage data (Figure 4). The setting is available under Reports in the Settings section of the Microsoft 365 admin center.
If the organization opts to obfuscate usage data, the settings affects all admin centers and any other application that retrieves information using the Graph Reports API. To see the real data, you can change the setting programmatically before running a report and revert it afterwards. For instance, the user activity report script, which retrieves statistics about user activity across Exchange Online, Teams, SharePoint Online, OneDrive for Business, and Yammer over the last 180 days can’t work with obfuscated data because it would be impossible to collate data from multiple workloads, so it’s a good example of where obfuscation must be removed before the script runs.
Avoid Graph API Errors by Getting the Basics Right
Working with Graph APIs in PowerShell is not difficult if you get the basics right. Configure the app correctly and assign and consent to the necessary permissions should take care of most lurking issues. After that, the code is PowerShell and you can fix, adjust, adapt, and improve it as you like.
Finally, if scripts use the Microsoft Graph PowerShell SDK, make sure that you install and use the latest version of the module. Microsoft updates the SDK regularly to fix problems and introduce new cmdlets. It would be a pity to run into a problem just because a workstation runs an outdated module.
Microsoft Platform Migration Planning and Consolidation
Simplify migration planning, overcome migration challenges, and finish projects faster while minimizing the costs, risks and disruptions to users.
The Real Person!
The Real Person!
Thanks @Tony – I used it but was facing multiple issues
1. I remove the graph api url and use exchange powershell to get the room
2. integrate token to the for loop I can refresh the token as I am pulling report for 3 years
That’s fine. Whatever works for you is great by me. All I want to do is explain principles. After that, it’s up to you.
I will say that using the Microsoft Graph PowerShell SDK cmdlets instead of Graph API requests removes two common difficulties: token renewal and pagination. You could try that too.
Hi Tony. Thanks for another great article. I have one question in regards to connection/authentication to Graph.
Let’s say that I developed a PowerShell script, I connect to Graph API using certificate-based authentication from my own PC and I want to share the script with other team members. I assume that in this case each team member must install the same certificate on his/her PC in order to connect to Graph to be able to use my script, correct?
I guess that the easiest way would be to use a secret inside the script but as you mentioned it is recommended for test purposes only.
Basically, my goal is to share a script with other team member so that he/she could run it without any additional steps.
Thanks in advance for the reply.
The golden rule is that whoever runs a script has to prove their right to use the permissions required by the script. They can do that with:
Certificates
Stored credentials (in Azure Vault preferably)
Sign in as themselves and use a registered app with a secret
I think you’ll go down the credentials route. Or maybe use Azure Automation and a managed identity to run a job on a schedule. Or can they sign into Azure and execute a runbook?
Thanks for a quick reply Tony, really appreciated. Azure Automation is not a good solution in this case. I have a few complex PowerShell scripts with GUI and I do not really want others to have to modify them before first use.
I Just want them to run the script -> Sign in (as they do it nowadays for example when script requires connection to ExchangeOnline, MSOnline or PnP module) and that is all.
It’s a big change to move over from scripts where you get all the rights available to an admin when you sign in to the Graph permissions model. I must admit I haven’t thought too much about the issue of handling GUI-based script for personnel like help desk members. I think the certificate route is probably best.