Skip to main content

GDAP Ends Pre-Consent: How to Get Your Own M365 Automations Working Again

Nick Ross8 min read

TL;DR

  • Under DAP, adding a service principal to the Admin Agents group pre-consented your app registration into every customer tenant; GDAP removes that shortcut entirely.
  • Restoring partner automations under GDAP takes four building blocks: a dedicated service account, a Secure Application Model app registration, correctly scoped GDAP relationships, and an on-behalf-of consent call per customer.
  • The GDAP relationship needs a consent-capable role (Global Administrator, Application Administrator, or Cloud Application Administrator) plus every role that covers the API permissions your scripts actually call.
  • Microsoft began auto-transitioning DAP relationships to GDAP on May 22, 2023, which is the date most partner automations silently stopped working.
  • Changing your app registration's permissions later means removing the consent in each customer tenant and consenting again with the new scope.

PowerShell scripts, Power Automate flows, Graph API calls: most CSP partners run some mix of all three against customer tenants through delegated access, without ever logging in to those tenants directly. That entire model rests on an app registration in your own tenant, built on what Microsoft calls the Secure Application Model. GDAP changes the consent mechanics underneath it, and when the change hits, the scripts stop.

This guide covers the full repair for your own first-party automations: why they break, the four prerequisites, the token and consent scripts, bulk rollout, and what to do when you change permissions later. If a third-party vendor's integration broke rather than your own scripts, the vendor-facing version of this fix lives in the GDAP vendor integration playbook.

New to GDAP itself? Start with the background reading first: what GDAP is, a GDAP migration checklist, and using PIM with GDAP.

One date matters here: as of early May 2023, Microsoft's stated plan was to begin auto-transitioning DAP relationships to GDAP on May 22, 2023. If your automations died in late May 2023, that transition is almost certainly why.

Why GDAP breaks app registrations that worked for years

Partner access to customer tenants historically ran on Delegated Access Permissions (DAP), which granted Global Administrator rights into every downstream tenant. When you built an app registration under the Secure Application Model, you added its service principal to the Admin Agents group in Partner Center, and that single step let you generate refresh tokens and access tokens for every customer environment. No customer-side action was ever needed, which is why this arrangement is called pre-consent.

GDAP exists to shrink exactly that blast radius. Its layered protections against supply chain attacks are the point of the model, and pre-consent is one of the casualties. Under GDAP, your application must be explicitly consented into each downstream customer tenant. The good news: an on-behalf-of consent model lets you grant that consent in bulk, without touching tenants one at a time. The bad news: the setup involves several moving parts that have to line up precisely, which is what the rest of this guide walks through.

Prerequisite 1: a dedicated service account for automations

Do not run automations under a regular user account. People leave, passwords rotate, and your scripts inherit every one of those events. Create a dedicated account instead, with these requirements:

  • It needs the Global Administrator role at the moment you acquire access tokens in the later steps. It does not need standing GA rights, so the better pattern is Privileged Identity Management (PIM) (opens in new tab), making the account eligible for the role and activating it only when needed, which shrinks your attack surface.
  • It must have MFA, enforced through Conditional Access or per-user MFA settings. The MFA has to be Microsoft's. A third-party provider like Duo will not work here.
  • It must be a member of the Admin Agents group in Azure AD.

In this walkthrough the account is simply named Automations:

Creating a dedicated Automations user account in the admin center
Setting the basics for the new Automations service account

After creating the account, sign in once to complete MFA registration. In this tenant, Conditional Access policies enforce the MFA prompt at first sign-in:

First sign-in MFA registration for the service account enforced by Conditional Access

One operational tip: store this account's password and MFA token in a documentation tool such as IT Glue or Hudu, so more than one person can reach it when needed.

Prerequisite 2: the app registration (Secure Application Model)

The app registration is what mints your tokens and defines the permission ceiling for everything your automations do in customer tenants. It follows Microsoft's Secure Application Model (opens in new tab).

Already have an app registration from your DAP days? You can reuse it and skip the creation steps. Just verify the permission list still matches what your automations call.

Sign in to the Azure AD portal connected to your Partner Center environment, then go to Entra Admin Center > Applications > App Registrations:

App Registrations section in the Entra admin center

Select + New Registration. Give it a name, set it as a multi-tenant app, and add a Redirect URI:

Registering a new multi-tenant application with a redirect URI

From the app's overview page, copy the Application (client) ID and Directory (tenant) ID. Both feed the scripts later:

Application overview page showing client ID and tenant ID

Create a client secret next: add a new secret, pick a name and expiration window, then copy the value into a secure store (your documentation tool or Azure Key Vault) immediately:

Generating a client secret for the app registration

Add the API permissions your automations call

Under + Add a permission, select the scope for each API your scripts use. Microsoft Graph covers most cases since it is the broadest API surface. Add every Graph permission your API calls and cmdlets require (the delegated-versus-application distinction is out of scope for this article):

Adding Microsoft Graph permissions to the app registration

Also add Microsoft Partner Center APIs for user impersonation:

Adding Partner Center user impersonation permission

If you automate Exchange Online, add the Exchange Online APIs as well. Search using this application ID: 00000002-0000-0ff1-ce00-000000000000

Searching for the Exchange Online API by application ID

The permission that matters for Exchange automation is Exchange.Manage:

Selecting the Exchange.Manage permission

Finish by clicking Grant Admin Consent for your org. You must be signed in as a Global Admin for this to succeed. The permissions shown below are a minimal example; a production registration will usually carry many more Graph permissions:

Granting admin consent for the configured permissions

Prerequisite 3: GDAP relationships with the right roles and group

Every customer your automations touch needs a GDAP relationship, and the roles inside that relationship decide whether any of this works. Two separate requirements stack here:

  1. Consent capability. To use the on-behalf-of consent model at all, the relationship must include at least one of: Global Admin, Application Admin, or Cloud Application Admin.
  2. Permission coverage. The relationship must also include roles that encompass the API calls your app makes. If your app registration reads Intune configuration profiles, the relationship needs the Intune Administrator role, and so on for each workload.

Then comes the step people miss: assign a security group to each GDAP relationship, and make your automation service account a member of that group. This example uses a group named GDAP_SG1 with the service account as its member:

Security group GDAP_SG1 containing the automation service account

In Partner Center, confirm the security group is assigned to each GDAP relationship with at least one of these permissions: Global Admin, Privilege Role Admin, or Cloud Application Admin.

Assigning the security group with admin roles to a GDAP relationship in Partner Center

Nothing in the following sections works until this is configured correctly, so check it twice.

Step 1: acquire a partner access token

With the prerequisites in place, use the app registration to generate a refresh token and access token. These examples use PowerShell, though the raw API flow (opens in new tab) works equally well. Fill in the variables from the app registration you created:

powershell
$AppId = 'Your Application ID'
$AppSecret = 'Your Application/Client Secret'
$consentscope = 'https://api.partnercenter.microsoft.com/user_impersonation'
$AppCredential = (New-Object System.Management.Automation.PSCredential ($AppId, (ConvertTo-SecureString $AppSecret -AsPlainText -Force)))
$PartnerTenantid = 'Your TenantID'
$AppDisplayName = 'Your Application Display Name'

# Get PartnerAccessToken token
$PartnerAccessToken = New-PartnerAccessToken -serviceprincipal -ApplicationId $AppId -Credential $AppCredential -Scopes $consentscope -tenant $PartnerTenantid -UseAuthorizationCode

Use PowerShell v5 or later; this walkthrough was tested on v7.0.

Running the script prompts a sign-in. Use the automation service account here (with the Global Admin role activated first if you went the PIM route). On success, calling the $PartnerAccessToken variable shows your refresh token and access token:

PartnerAccessToken output showing the refresh and access tokens

Store the refresh token in a secure location like Azure Key Vault. It mints new access tokens and must be renewed every 90 days. Gavin's walkthrough on storing and retrieving secrets from Azure Key Vault (opens in new tab) covers the pattern well.

There is also speculation that Microsoft does not intend to maintain the Partner Center PowerShell module long term. If you would rather skip the module, this sample calls the REST API directly:

powershell
# Define variables
$appId = 'your Azure AD application client ID'
$appSecret = 'your Azure AD application client secret'
$tenantId = 'your Azure AD tenant ID'
$scope = 'https://api.partnercenter.microsoft.com/.default'
$redirectUri = 'your redirect URI'

# Construct authorization endpoint URL
$authEndpoint = "https://login.microsoftonline.com/$tenantId/oauth2/v2.0/authorize?client_id=$appId&response_type=code&redirect_uri=$redirectUri&scope=$scope"

# Navigate to authorization endpoint and obtain authorization code
Start-Process $authEndpoint
$code = Read-Host "Enter authorization code"

$body = "grant_type=authorization_code&client_id=$appId&client_secret=$appSecret&code=$code&redirect_uri=$redirectUri&scope=$scope"
$headers = @{ 'Content-Type' = 'application/x-www-form-urlencoded' }
$tokenEndpoint = "https://login.microsoftonline.com/$tenantId/oauth2/token"
$response = Invoke-RestMethod -Method POST -Uri $tokenEndpoint -Body $body -Headers $headers

$AccessToken = $response.Access_Token

The authorization code comes straight out of the redirect URL after sign-in:

Browser URL containing the authorization code parameter

The access token unlocks the API Microsoft built for exactly this: granting application consent into downstream tenants. The PowerShell cmdlet wraps what Microsoft calls the CPV consent API. Ignore the Control Panel Vendor naming; CSPs can call it too.

Cmdlet reference: New-PartnerCustomerApplicationConsent (opens in new tab). API reference: Control Panel Vendor APIs for customer consent (opens in new tab).

powershell
$CustomerTenantId = 'Your Customer TenantID'
# Connect using PartnerAccessToken token
$PartnerCenter = Connect-PartnerCenter -AccessToken $PartnerAccessToken.AccessToken
#Grants needed
$MSGraphgrant = New-Object -TypeName Microsoft.Store.PartnerCenter.Models.ApplicationConsents.ApplicationGrant
$MSgraphgrant.EnterpriseApplicationId = "00000003-0000-0000-c000-000000000000"
$MSGraphgrant.Scope = "Device.ReadWrite.All,User.Read"
$ExOgrant = New-Object -TypeName Microsoft.Store.PartnerCenter.Models.ApplicationConsents.ApplicationGrant
$ExOgrant.EnterpriseApplicationID = "00000002-0000-0ff1-ce00-000000000000"
$ExOgrant.Scope = "Exchange.Manage"
New-PartnerCustomerApplicationConsent -ApplicationGrants @($MSGraphgrant, $ExOgrant) -CustomerId $CustomerTenantId -ApplicationId $AppId -DisplayName $appdisplayname

Two values to customize: the customer tenant ID you want to test against, and $MSGraphgrant.Scope, which should carry every permission from your app registration, comma separated. The example keeps it short for readability.

The same consent via REST, continuing from the access token acquired above:

powershell
$CustomerTenantId = 'Your Customer TenantID'

$headers = @{
    Authorization = "Bearer $($AccessToken)"
    'Accept'      = 'application/json'
}

 # Consent to required applications
    $uri = "https://api.partnercenter.microsoft.com/v1/customers/$CustomerTenantId/applicationconsents"
    $body = @{
        applicationGrants = @(
            @{
                enterpriseApplicationId = "00000003-0000-0000-c000-000000000000"
                scope                   = "Directory.Read.All,Directory.AccessAsUser.All"
            },
            @{
                enterpriseApplicationId = "00000002-0000-0ff1-ce00-000000000000"
                scope                   = "Exchange.Manage"
            }
        )
        applicationId   = $AppId
        displayName     = $AppDisplayName
    } | ConvertTo-Json

    Invoke-RestMethod -Uri $uri -Headers $headers -Method POST -Body $body -ContentType 'application/json'

Decoding the errors you might hit

Error response complaining about the application identifier

This error with a 100% correct application ID usually means an old PowerShell version. It appeared here while testing on PowerShell v3.

Authorization error returned by the consent API

This one points at the service account: it is either missing from the Admin Agents group, or not a member of the security group assigned to the customer's GDAP relationship.

Successful consent API response output

That is the success case.

Trust but verify. Sign in to the customer's Entra admin center, open Enterprise Applications, and clear the Application Type filter by clicking its X:

Clearing the application type filter in Enterprise Applications

Search for your app registration's name; it should appear in the list:

The consented application listed in the customer tenant

Clicking into Permissions shows every grant the script defined:

Permissions on the consented application matching the script's scope

From here, token generation and API calls work for this customer. A sample that pulls devices via Graph:

powershell
$graphToken = New-PartnerAccessToken -ApplicationId $AppId -Credential $appcredential -RefreshToken $PartnerAccesstoken.refreshToken -Scopes 'https://graph.microsoft.com/.default' -ServicePrincipal -Tenant $CustomerTenantId
$headers = @{ "Authorization" = "Bearer $($graphToken.AccessToken)" }
$Devices = (Invoke-RestMethod -Uri 'https://graph.microsoft.com/beta/devices' -Headers $headers -Method Get -ContentType "application/json").value
Device list returned from the Graph beta endpoint for the customer tenant

Two ways to connect to Exchange Online for a customer:

powershell
$token = New-PartnerAccessToken -ApplicationId $AppId -Scopes 'https://outlook.office365.com/.default' -ServicePrincipal -Credential $appcredential -Tenant $CustomerTenantId -RefreshToken $PartnerAccesstoken.refreshToken
Connect-ExchangeOnline -DelegatedOrganization $CustomerTenantId -AccessToken $token.AccessToken
powershell
# Define ExchangeTokenSplat parameters
$ExchangeTokenSplat = @{
    ApplicationId = $AppId # AppID in CSP tenant
    Scopes = 'https://outlook.office365.com/.default'
    ServicePrincipal = $true
    Credential = (New-Object System.Management.Automation.PSCredential ($AppId, (ConvertTo-SecureString $AppSecret -AsPlainText -Force)))
    RefreshToken = $RefreshToken
    Tenant = $CustomerTenantId # Customer TenantID
}

# Get $ExchangeToken
$ExchangeToken = New-PartnerAccessToken @ExchangeTokenSplat

# Connect to MgGraph
Connect-ExchangeOnline -DelegatedOrganization $CustomerTenantId -AccessToken $ExchangeToken.AccessToken

The same flow viewed from Postman. First, a token request failing because consent has not been granted yet:

Postman request failing to get an access token before consent

Then a successful token request after consent:

Postman successfully returning an access token after consent

And a successful API call against the customer using the app registration's permissions:

Postman API call returning customer data with the consented permissions

One customer down, the rest to go. Several APIs return your full customer list for looping:

Common options:

Newer option:

A sample bulk-consent script:

powershell
#variables
$AppId = ''
$AppSecret = ''
$consentscope = 'https://api.partnercenter.microsoft.com/user_impersonation'
$AppCredential = (New-Object System.Management.Automation.PSCredential ($AppId, (ConvertTo-SecureString $AppSecret -AsPlainText -Force)))
$PartnerTenantid = ''
$AppDisplayName = ''

# Get PartnerAccessToken token
$PartnerAccessToken = New-PartnerAccessToken -serviceprincipal -ApplicationId $AppId -Credential $AppCredential -Scopes $consentscope -tenant $PartnerTenantid -UseAuthorizationCode

# Connect using PartnerAccessToken token
$PartnerCenter = Connect-PartnerCenter -AccessToken $PartnerAccessToken.AccessToken

#Grants needed
$MSGraphgrant = New-Object -TypeName Microsoft.Store.PartnerCenter.Models.ApplicationConsents.ApplicationGrant
$MSgraphgrant.EnterpriseApplicationId = "00000003-0000-0000-c000-000000000000"
$MSGraphgrant.Scope = "Directory.Read.All,Directory.AccessAsUser.All"
$ExOgrant = New-Object -TypeName Microsoft.Store.PartnerCenter.Models.ApplicationConsents.ApplicationGrant
$ExOgrant.EnterpriseApplicationID = "00000002-0000-0ff1-ce00-000000000000"
$ExOgrant.Scope = "Exchange.Manage"

# Get list of customers
$Customers = Get-PartnerCustomer

# Loop through each customer and run the existing script
foreach ($Customer in $Customers) {
    $CustomerTenantId = $Customer.CustomerId

    # Consent to required applications
    New-PartnerCustomerApplicationConsent -ApplicationGrants @($MSGraphgrant, $ExOgrant) -CustomerId $CustomerTenantId -ApplicationId $AppId -DisplayName $AppDisplayName

}

And the REST equivalent:

powershell
# Define variables
$appId = 'your Azure AD application client ID'
$appSecret = 'your Azure AD application client secret'
$tenantId = 'your Azure AD tenant ID'
$scope = 'https://api.partnercenter.microsoft.com/.default'
$redirectUri = 'your redirect URI'

# Construct authorization endpoint URL
$authEndpoint = "https://login.microsoftonline.com/$tenantId/oauth2/v2.0/authorize?client_id=$appId&response_type=code&redirect_uri=$redirectUri&scope=$scope"

# Navigate to authorization endpoint and obtain authorization code
Start-Process $authEndpoint
$code = Read-Host "Enter authorization code"

$body = "grant_type=authorization_code&client_id=$appId&client_secret=$appSecret&code=$code&redirect_uri=$redirectUri&scope=$scope"
$headers = @{ 'Content-Type' = 'application/x-www-form-urlencoded' }

$response = Invoke-RestMethod -Method POST -Uri $tokenEndpoint -Body $body -Headers $headers

$AccessToken = $response.AccessToken

$CustomerTenantId = 'Your Customer TenantID'

# Get list of customers
$uri = "https://api.partnercenter.microsoft.com/v1/customers"
$headers = @{
    Authorization = "Bearer $($AccessToken)"
    'Accept'      = 'application/json'
}

$Customers = Invoke-RestMethod -Uri $uri -Headers $headers

# Loop through each customer and run the existing script
foreach ($Customer in $Customers.value) {
    $CustomerTenantId = $Customer.id

    # Consent to required applications
    $uri = "https://api.partnercenter.microsoft.com/v1/customers/$CustomerTenantId/applicationconsents"
    $body = @{
        applicationGrants = @(
            @{
                enterpriseApplicationId = "00000003-0000-0000-c000-000000000000"
                scope                   = "Directory.Read.All,Directory.AccessAsUser.All"
            },
            @{
                enterpriseApplicationId = "00000002-0000-0ff1-ce00-000000000000"
                scope                   = "Exchange.Manage"
            }
        )
        applicationId   = $AppId
        displayName     = $AppDisplayName
    } | ConvertTo-Json

  Invoke-RestMethod -Uri $uri -Headers $headers -Method POST -Body $body -ContentType 'application/json'

  }

What happens when you change permissions later

Adding permissions to the app registration means re-consenting across every customer, and as of May 2023 it is not as simple as re-running the consent script. You have to remove the existing application consent and add it back with the updated scope. There is no direct PowerShell cmdlet for the removal yet, but the API supports it: remove consent via the CPV APIs (opens in new tab). Remove, then re-consent with the new scope.

The same logic applies to your vendors

Any vendor whose integration reaches your downstream customers through an app registration is subject to the same GDAP mechanics. To keep those integrations alive, you will be doing a familiar subset of this work on their behalf:

  • Creating a service account specific to the integration
  • Adding that service account to the GDAP relationships
  • Ensuring the GDAP relationship roles cover everything the integration needs

The vendor-side details are covered in the companion guide for vendor integrations.

Reference material

Frequently asked questions

Do you have to consent customer by customer?

No. The on-behalf-of consent API accepts any customer tenant ID, so you can loop a list of customers from Partner Center or Microsoft Graph and grant consent to all of them in one script run. You never sign in to each customer tenant.

Why does the consent call fail with an authorization error even though the app ID is correct?

The two common causes are the service account missing from the Admin Agents group, or the service account not being a member of the security group assigned to that customer's GDAP relationship. An app-identifier error with a correct app ID usually means an outdated PowerShell version.

How long does the partner refresh token last?

90 days. Store it somewhere like Azure Key Vault and schedule a renewal job, because every Graph and Exchange token you mint for customer tenants derives from it.

Know which tenants your app can actually reach

Consent drift is real: new customers get added, GDAP relationships expire, and scripts fail quietly. CloudCapsule checks 250+ controls across every tenant you manage in about 60 seconds, so gaps surface before a script run does.

Run a free scan
Nick Ross

Written by

Nick Ross

CEO · Microsoft MVP · Founder, T-Minus 365

Nick is not just a CEO, he's a respected thought leader and influencer in the MSP space. Tens of thousands of MSPs learn through his YouTube channel, T-Minus365. Nick has been honored as a three-time Microsoft MVP for his educational content; his expertise and influence are the backbone of our mission, ensuring that you are in the best hands when it comes to security.

Nick joined Pax8 in 2017, where he would ultimately oversee product management for PSA and Microsoft integrations. Following his tenure at Pax8, Nick has continued to demonstrate his leadership prowess as an executive at various MSPs, culminating in his most recent role at Sourcepass.

Nick holds a Bachelor's Degree in Business Management from Florida State University, as well as a Minor Degree in Entrepreneurship. In his free time, Nick is an avid hiker, reader, and fitness-junkie.

Keep reading