Using the Microsoft Graph PowerShell module with GDAP
May 27, 2024โข1,327 words
Overview
GDAP (Granular Delegated Admin Privileges) improves security over DAP, but adds complexity. The goal of this post is to show you how to use Microsoft Graph PowerShell against your partner tenants. Unlike other guides you may have seen, I'll be avoiding the PartnerCenter module and using MSAL.PS for authentication. Unfortunately, the PartnerCenter has some issues in combination with the Microsoft Graph module and hasn't had any updates since 2020. Token storage for automation is not covered.
Walkthrough
Create your Partner Application
Start by creating your partner application. In Azure AD:
- Create a multi-tenant App Registration. Name it something like "Partner App".
- Add delegated permissions for Microsoft Partner Center > user_impersonation.
- Under Authentication, add Mobile and Desktop Applications and then add https://localhost as a Customer Redirect URI.
Install PowerShell 7 (optional with tweaks)
Next, for this guide, I'm going to recommend you install the latest version of PowerShell 7. This is solely because I use the Microsoft.PowerShell.ConsoleGuiTools
module to select tenants. If you want to adjust that part to get your customer(s) as different way, you can continue to use Windows PowerShell 5.1.
Install Modules
After you do that (or adjust the scripts), install the following modules:
Install-Module Microsoft.Graph -Scope CurrentUser
Install-Module MSAL.PS -Scope CurrentUser
Install-Module Microsoft.PowerShell.ConsoleGuiTools -Scope CurrentUser
Install-Module ExchangeOnlineManagement -Scope CurrentUser
Run a script to consent your app and permissions to tenant(s)
Copy the following script somewhere. This script is what we'll use to consent the Microsoft Graph and Exchange Online 1st party Enterprise Application on behalf of our tenants. When we run this, I believe you'll need to do so from an account that is in a group that has the Global Administrator and Application Administrator GDAP role.
#Requires -Version 7.2
#Requires -Modules MSAL.PS, Microsoft.PowerShell.ConsoleGuiTools
param(
[Parameter(
Mandatory = $true,
HelpMessage = @"
Create an App registration with the following:
- For permissions, added Microsoft Partner Center > user_impersonation as a delegated permission
- Under Authentication, add Mobile and Device Applications and add https://localhost as a Custom Redirect URI
Use the Application ID for this parameter.
"@)]
[string] $PartnerAppId,
[Parameter(
HelpMessage = @"
This is the list of scopes you'll need for your script or automation. Example:
Directory.Read.All,AccessReview.Read.All
"@)]
[string] $Scope
)
$ErrorActionPreference = "stop"
$MsalParams = @{
ClientId = $PartnerAppId;
Interactive = $true;
Scope = "https://api.partnercenter.microsoft.com/user_impersonation";
}
$PartnerAccessToken = Get-MsalToken @MsalParams
$PartnerCustomersParams = @{
Uri = "https://api.partnercenter.microsoft.com/v1/customers";
Headers = @{
"Authorization" = "Bearer $($PartnerAccessToken.AccessToken)"
};
Method = "Get";
}
$SelectedCustomers = (Invoke-RestMethod @PartnerCustomersParams).items | `
Select-Object -ExpandProperty companyProfile | `
Select-Object -Property companyName, tenantId | `
Out-ConsoleGridView -OutputMode Multiple -Title "Select your tenant"
<#
This step is what will add the application to your customer's organization.
The EnterpriseAppplicationId below is the app you're allowing to use your partner credentials.
In this example, it's just the Microsoft Graph enterprise application.
Notice how the scope isn't in our Partner App. You don't need to specify it in the partner app.
If you change the scope, it should add the ones you didn't include, but I don't see a way to remove any.
Your user account's permissions through GDAP are what determines what you really have permissions to so if your user
didn't have Directory.Read.All permissions, this wouldn't work.
Since there's no way to check or force this to update, it will throw an error-like message if you've already done this.
#>
# Forcing a scope selection from the list if one wasn't specified.
if (!$Scope) {
$SelectedScopes = Find-MgGraphPermission | `
Where-Object PermissionType -eq "Delegated" | `
Select-Object Name, Description | `
Out-ConsoleGridView -OutputMode Multiple -Title "Select your scopes"
$Scope = $SelectedScopes.Name -Join ","
if (!$Scope) {
throw "Selecting a scope(s) is required to continue."
}
}
$AppConsentHeaders = @{
"Accept" = "application/json";
"Content-Type" = "application/json";
"Authorization" = "Bearer $($PartnerAccessToken.AccessToken)"
}
$AppConsentBody = ConvertTo-JSON -InputObject @{
"applicationid" = $PartnerAppId;
"applicationGrants" = @(
@{
"EnterpriseApplicationId" = "00000003-0000-0000-c000-000000000000"; # Microsoft Graph.
"Scope" = $Scope
},
@{
"EnterpriseApplicationId" = "00000002-0000-0ff1-ce00-000000000000"; # Exchange Online.
"Scope" = "Exchange.Manage"
}
)
}
$Index = 0
ForEach ($Customer in $SelectedCustomers) {
$Index++
$PercentComplete = [int][math]::Ceiling(100 / $SelectedCustomers.Count * $Index )
Write-Progress `
-Activity $Customer.companyName `
-Status "$Index out of $($SelectedCustomers.Count)" `
-PercentComplete $PercentComplete
$CustomerTenantId = $Customer.tenantId
# Removing existing consent if it exists. There isn't a way to query or updates, unfortunately.
$PartnerCustomerRemoveAppConsentParams = @{
Uri = "https://api.partnercenter.microsoft.com/v1/customers/$CustomerTenantId/applicationconsents/$PartnerAppId";
Headers = $AppConsentHeaders;
Method = "DELETE";
}
Invoke-RestMethod @PartnerCustomerRemoveAppConsentParams
# Adding new consents.
$PartnerCustomerPostAppConsentParams = @{
Uri = "https://api.partnercenter.microsoft.com/v1/customers/$CustomerTenantId/applicationconsents";
Headers = $AppConsentHeaders;
Body = $AppConsentBody
Method = "POST";
}
Invoke-RestMethod @PartnerCustomerPostAppConsentParams | Out-Null
}
Go ahead and run this script using the Application ID you created earlier for $PartnerAppId
and a comma-separated list of scopes that you're comfortable with. If you're just testing, Directory.Read.All
should be fine. You'll also have the option to select a single or multiple tenants to do this with.If you saved the script as Update-PartnerAppConsent.ps
, you'd run:
.\Update-PartnerAppConsent.ps -ApplicationID "{YOUR_PARTNER_APP_ID}" -Scope "Directory.Read.All"
If you're okay with adding the full list of scopes, you should restrict this to any scope anyone in your organization will need. Their permissions will be restricted by GDAP, so they won't be able to get to anything if their role doesn't allow it. By default, the only one pre-included is for Exchange Online.
Example Script: Get users and mailboxes for a tenant
Copy the following script to your system. We'll use this for testing getting a list of a tenant's users along with their mailboxes.
#Requires -Version 7.2
#Requires -Modules MSAL.PS, Microsoft.PowerShell.ConsoleGuiTools, ExchangeOnlineManagement
$ErrorActionPreference = "stop"
$PartnerAppId = "{PartnerAppId}"
$MsalParams = @{
ClientId = $PartnerAppId;
Interactive = $true;
Scope = "https://api.partnercenter.microsoft.com/user_impersonation";
}
$PartnerAccessToken = Get-MsalToken @MsalParams
$PartnerCustomersParams = @{
Uri = "https://api.partnercenter.microsoft.com/v1/customers";
Headers = @{
"Authorization" = "Bearer $($PartnerAccessToken.AccessToken)"
};
Method = "Get";
}
# Use this cool module to select a single customer from the list.
$Customer = (Invoke-RestMethod @PartnerCustomersParams).items | `
Select-Object -ExpandProperty companyProfile | `
Select-Object -Property companyName, tenantId | `
Out-ConsoleGridView -OutputMode Single -Title "Select your tenant"
# Get tokens for Exchange Online and Microsoft Graph
$MsalCustomerGraphParams = @{
ClientId = $PartnerAppId;
TenantId = $Customer.tenantId;
Scope = "Directory.Read.All"; # Change this depending on what you're doing below
Silent = $true; # Uses the saved refresh token
}
$MsalCustomerEXOParams = @{
ClientId = $PartnerAppId;
TenantId = $Customer.tenantId;
Scope = "https://outlook.office365.com/.default";
Silent = $true; # Uses the saved refresh token
}
$CustomerGraphAccessToken = Get-MsalToken @MsalCustomerGraphParams
$CustomerEXOAccessToken = Get-MsalToken @MsalCustomerEXOParams
# Connect to Exchange Online and Microsoft Graph and run commands
Connect-ExchangeOnline -DelegatedOrganization $Customer.tenantId -AccessToken $CustomerEXOAccessToken.AccessToken
Connect-MgGraph -AccessToken $CustomerGraphAccessToken.AccessToken
Get-MgUser
Get-ExoMailbox
The only two things you need now to make this work are:
- Replace the PartnerAppId value in the script above with your actual Application ID. I didn't use a variable in this case since it's not sensitive and I can imagine you'd want to create scripts that can be ran as-is by your employees.
- Update your
$MsalCustomerGraphParams
scope to whatever is relevant to the actual thing you're trying to do, then replace everything belowConnect-MgGraph
with what you actually want to do.
Resources
This blog post from tminus365 is a great reference for how to design your GDAP groups. It's important to have this exactly the way you want so you don't have to redo any of it later. I used their post in combination with the following to decide on what roles I needed:
The main difference for us was we decided Global Reader would be appropriate for all of our staff. I imagine there are certain types of customers where this wouldn't be appropriate, but it works for us.
Another thing that isn't clearly documented anywhere I can find is what Azure AD roles map to what Microsoft Graph scopes. Fortunately, I haven't had a hard time guessing based on the descriptions, but it is a huge pain if you're trying to run something quickly.
Another resource I found was this, which explains how to connect to the new Exchange Online module with GDAP. The sample got me started, even though I ultimately ended up using the APIs directly.