Skip to main content

Introduction

It’s very common for your end-users to want to use Microsoft SSO to login to your client portal. In order for that to happen though, someone - either in your organization, or a privileged user within your customer’s organization - needs to approve the installation of the “MSP Process SSO” enterprise app. Depending on the security settings within your customer’s M365 tenant, they may not be able to approve the installation of the MSP Process SSO enterprise app - this KB article walks you through how to pre-approve it, so that your rollout of the Client Portal is as smooth and user-friendly as possible.

How it works

Customer end-users sign in to the client portal (and it’s Teams app variant) through the MSP Process SSO enterprise application. By default, the first user to login user is asked to approve the app the first time, and in many tenants they aren’t allowed to. Granting admin consent in the customer’s Microsoft tenant approves the app for all of that tenant’s users at once, so nobody sees a prompt. Consent is granted once per customer tenant. You’ll likely use a combination of the two methods below - the Partner Center approach as the first step, and then the per-tenant Admin consent link for any outliers.

App details

Get the customer’s tenant ID (a GUID) or a verified domain such as contoso.com . Build the link below, replacing {TENANT} with the tenant ID or domain (in both places):
  1. https://login.microsoftonline.com/\{TENANT\}/adminconsent?client\_id=b38f995e-3812-43f1-97d5-5c4ff7578286&redirect\_uri=https%3A%2F%2Fapp.mspprocess.com%2F&state=\{TENANT\}
  2. Open the link while signed in as an admin of the customer tenant. This can be a customer admin, or you through GDAP. The account needs one of these roles in the customer tenant: Global Administrator, Privileged Role Administrator, Cloud Application Administrator, or Application Administrator.
  3. Review the consent screen. It shows the verified badge for MSP Process. Select Accept.
  4. You are redirected to https://app.mspprocess/com/ with admin_consent=True and the tenant ID in the address. That confirms it worked.
Use the customer’s own tenant ID /common and /organizations don’t work in this link. It must be the customer’s tenant ID or domain, not your own.

Option B: Script through Admin Relationships

If you manage many customers through Admin Relationships in the Microsoft Partner Center, the script below does Option A’s work for all of them in one run. LimitationThe script signs in with Microsoft’s “Microsoft Graph Command Line Tools” app. In customer tenants where that app hasn’t been authorized, sign-in fails with AADSTS90099 , and GDAP can’t work around it. For those customers, use Option A. The script marks them Failed and includes the link for you.

Prerequisites

  1. An active Admin Relationship with each customer that includes Cloud Application Administrator (or Application Administrator).
  2. That role assigned to a security group in your tenant, with you as a member.
  3. PowerShell 7 and the Microsoft Graph SDK:
Install-Module Microsoft.Graph -Scope CurrentUser

Download and Run the script

Save the PowerShell code below as Grant-MspProcessSsoConsent.ps1 : <#.SYNOPSISPre-approves the “MSP Process SSO” enterprise app in every GDAP customer tenant,so end-users can sign in to the MSP Process client portal with Microsoft SSOwithout seeing a consent prompt. .DESCRIPTIONRun by a technician in the MSP (partner) tenant. For each customer:1. Finds active GDAP relationships that include Cloud Application Administratoror Application Administrator (or uses the -TenantId list you supply).2. Signs in to the customer tenant through GDAP.3. Creates the MSP Process SSO service principal if it doesn’t exist.4. Grants tenant-wide (AllPrincipals) delegated consent for the scopes below.5. Verifies the result and writes a CSV report.Safe to re-run: existing service principals and grants are reused or updated. .PARAMETER TenantIdOne or more customer tenant GUIDs. If omitted, tenants come from active GDAP relationships. .PARAMETER OutputPathCSV report path. Default: .\MspProcessSso-Consent-<timestamp>.csv .EXAMPLE.\Grant-MspProcessSsoConsent.ps1 -WhatIf.\Grant-MspProcessSsoConsent.ps1.\Grant-MspProcessSsoConsent.ps1 -TenantId 11111111-2222-3333-4444-555555555555#>[CmdletBinding(SupportsShouldProcess)]param([string[]]TenantId,\[string\]OutputPath = ”.\MspProcessSso-Consent-$(Get-Date -Format ‘yyyyMMdd-HHmmss’).csv”) $ErrorActionPreference = ‘Stop’ # ---- MSP Process SSO settings --------------------------------------------AppId=′b38f995e−3812−43f1−97d5−5c4ff7578286′AppId = 'b38f995e-3812-43f1-97d5-5c4ff7578286'Scopes = ‘openid profile email User.Read’ # keep identical to the app’s API permissions$RedirectUri = ‘https://app.mspprocess.com/’ # must be a registered Web redirect URI# --------------------------------------------------------------------------- GraphAppId=′00000003−0000−0000−c000−000000000000′GraphAppId = '00000003-0000-0000-c000-000000000000'RoleCloudAppAdmin = ‘158c047a-c907-4556-b7ef-446551a6b5f7’$RoleAppAdmin = ‘9b895d92-2cd3-44c7-9d02-a6ac2d5ea5c3’ # Microsoft Graph PowerShell SDK (only the modules we need)foreach (min′Microsoft.Graph.Authentication′,′Microsoft.Graph.Applications′,′Microsoft.Graph.Identity.SignIns′,′Microsoft.Graph.Identity.Partner′){if(−not(Get−Module−ListAvailable−Namem in 'Microsoft.Graph.Authentication','Microsoft.Graph.Applications','Microsoft.Graph.Identity.SignIns','Microsoft.Graph.Identity.Partner') \{if (-not (Get-Module -ListAvailable -Name m)) {throw “Missing module m.Installwith:Install−ModuleMicrosoft.Graph−ScopeCurrentUser"}Import−Modulem. Install with: Install-Module Microsoft.Graph -Scope CurrentUser"\}Import-Module m -ErrorAction Stop} function Get-ConsentUrl([string]Tenant){Tenant) \{enc = [uri]::EscapeDataString(RedirectUri)"https://login.microsoftonline.com/RedirectUri)"https://login.microsoftonline.com/Tenant/adminconsent?client_id=AppId&redirect\_uri=enc&state=$Tenant”} # ---- Step 1: build the list of customer tenants ---------------------------$targets = @() if (TenantId){TenantId) \{targets = TenantId | ForEach-Object \{ \[pscustomobject\]@\{ TenantId = _; Name = ‘(supplied)’ } }}else {Write-Host “Signing in to your PARTNER tenant to list GDAP relationships…” -ForegroundColor CyanConnect-MgGraph -Scopes ‘DelegatedAdminRelationship.Read.All’ -NoWelcome -ContextScope Process rels=Get−MgTenantRelationshipDelegatedAdminRelationship−All−Filter"statuseq′active′"rels = Get-MgTenantRelationshipDelegatedAdminRelationship -All -Filter "status eq 'active'"byTenant = @{}foreach (rinr in rels) {tid=tid = r.Customer.TenantIdif (-not tid){continue}tid) \{ continue \}roles = @(r.AccessDetails.UnifiedRoles.RoleDefinitionId)r.AccessDetails.UnifiedRoles.RoleDefinitionId)hasRole = (roles−containsroles -contains RoleCloudAppAdmin) -or (roles−containsroles -contains RoleAppAdmin)if (-not byTenant.ContainsKey(byTenant.ContainsKey(tid)) {byTenant\[byTenant\[tid] = [pscustomobject]@{ TenantId = tid;Name=tid; Name = r.Customer.DisplayName; HasRole = hasRole}}elseif(hasRole \}\}elseif (hasRole) { byTenant\[byTenant\[tid].HasRole = $true }}Disconnect-MgGraph | Out-Null targets=targets = byTenant.Values | Sort-Object NameWrite-Host (“Found {0} customer tenants with active GDAP.” -f $targets.Count)} # ---- Step 2: process each customer ---------------------------------------$results = New-Object System.Collections.Generic.List[object] foreach (tint in targets) {row = \[ordered\]@\{Customer = t.Name; TenantId = t.TenantId;Status=′′;Detail=′′ServicePrincipalId=′′;GrantedScopes=′′;FallbackConsentUrl=′′}Write−Host"nˋ==t.TenantId; Status = ''; Detail = ''ServicePrincipalId = ''; GrantedScopes = ''; FallbackConsentUrl = ''\}Write-Host "\`n== (t.Name)\[t.Name) \[($t.TenantId)]” -ForegroundColor Cyan if (t.PSObject.Properties.Name−contains′HasRole′−and−nott.PSObject.Properties.Name -contains 'HasRole' -and -not t.HasRole) {row.Status=′Skipped′row.Status = 'Skipped'row.Detail = ‘GDAP relationship lacks Cloud Application Administrator / Application Administrator’row.FallbackConsentUrl=Get−ConsentUrlrow.FallbackConsentUrl = Get-ConsentUrl t.TenantIdWrite-Warning row.Detailrow.Detailresults.Add([pscustomobject]$row); continue} if (-not PSCmdlet.ShouldProcess(PSCmdlet.ShouldProcess(t.TenantId, “Create service principal and grant tenant-wide consent for Scopes")){Scopes")) \{row.Status = ‘WhatIf’results.Add(\[pscustomobject\]row); continue} try {Connect-MgGraph -TenantId $t.TenantId `-Scopes ‘Application.ReadWrite.All’, ‘DelegatedPermissionGrant.ReadWrite.All’ `-NoWelcome -ContextScope Process # Service principal for MSP Process SSOsp=Get−MgServicePrincipal−Filter"appIdeq′sp = Get-MgServicePrincipal -Filter "appId eq 'AppId’” | Select-Object -First 1created=created = falseif (-not sp){sp) \{sp = New-MgServicePrincipal -AppId AppIdAppIdcreated = $trueWrite-Host ” Created enterprise app.”} else { Write-Host ” Enterprise app already present.” } # Microsoft Graph service principal in this tenantgraph=Get−MgServicePrincipal−Filter"appIdeq′graph = Get-MgServicePrincipal -Filter "appId eq 'GraphAppId’” | Select-Object -First 1 # Tenant-wide delegated grant (retry to ride out replication delay after creation)grant=grant = nullfor (i=1;i = 1; i -le 6; i++){try{i++) \{try \{grant = Get-MgOauth2PermissionGrant -Filter “clientId eq ‘((sp.Id)’ and resourceId eq ‘((graph.Id)’” |Where-Object ConsentType -eq ‘AllPrincipals’ | Select-Object -First 1if (-not grant){grant) \{grant = New-MgOauth2PermissionGrant -ClientId sp.Id−ConsentType′AllPrincipals′−ˋResourceIdsp.Id -ConsentType 'AllPrincipals' \`-ResourceId graph.Id -Scope ScopesWrite−Host"Grantedtenant−wideconsent."}elseif(((ScopesWrite-Host " Granted tenant-wide consent."\}elseif (((grant.Scope -split ‘\s+’ | Where-Object { \_ \}) | Sort-Object) -join ' ' \`-ne ((Scopes -split ‘\s+’) | Sort-Object) -join ’ ’) {Update-MgOauth2PermissionGrant -OAuth2PermissionGrantId grant.Id−Scopegrant.Id -Scope ScopesWrite-Host ” Updated existing grant scopes.”}else { Write-Host ” Consent already granted.” }break}catch {if (i−eq6){throw}Start−Sleep−Seconds(3\*i -eq 6) \{ throw \}Start-Sleep -Seconds (3 \* i)}} # Verifyverify=Get−MgOauth2PermissionGrant−Filter"clientIdeq′verify = Get-MgOauth2PermissionGrant -Filter "clientId eq '(sp.Id)′"∣Where−ObjectConsentType−eq′AllPrincipals′∣Select−Object−First1if(−notsp.Id)'" |Where-Object ConsentType -eq 'AllPrincipals' | Select-Object -First 1if (-not verify) { throw ‘Verification failed: no AllPrincipals grant found after granting.’ } row.Status=if(row.Status = if (created) { ‘Created+Consented’ } else { ‘Consented’ }row.ServicePrincipalId=row.ServicePrincipalId = sp.Idrow.GrantedScopes=(row.GrantedScopes = (verify.Scope.Trim())}catch {row.Status=′Failed′row.Status = 'Failed'row.Detail = _.Exception.Messageif(\_.Exception.Messageif (row.Detail -match ‘AADSTS90099’) {row.Detail=′AADSTS90099:MicrosoftGraphCommandLineToolsisnotauthorizedinthiscustomertenant,soGDAPcannotuseit.UsetheFallbackConsentUrl(adminconsentlink)forthiscustomerinstead.′}row.Detail = 'AADSTS90099: Microsoft Graph Command Line Tools is not authorized in this customer tenant, so GDAP cannot use it. Use the FallbackConsentUrl (admin consent link) for this customer instead.'\}row.FallbackConsentUrl = Get-ConsentUrl t.TenantIdWrite−Warning"t.TenantIdWrite-Warning " (row.Detail)"}finally{Disconnect−MgGraph−ErrorActionSilentlyContinue∣Out−Null}row.Detail)"\}finally \{Disconnect-MgGraph -ErrorAction SilentlyContinue | Out-Null\}results.Add([pscustomobject]$row)} # ---- Step 3: report --------------------------------------------------------results∣Export−Csv−Pathresults | Export-Csv -Path OutputPath -NoTypeInformation -Encoding UTF8Write-Host “`nSummary:” -ForegroundColor Greenresults∣Group−ObjectStatus∣ForEach−Object{Write−Host("{0,−18}{1}"−fresults | Group-Object Status | ForEach-Object \{ Write-Host (" \{0,-18\} \{1\}" -f _.Name, _.Count)}Write−Host"Reportwrittento:\_.Count) \}Write-Host "Report written to: OutputPath”Write-Host “Send the TenantId column to MSP Process (or register it in the portal) so sign-ins can be matched to the right customer.”if (results.Where({results.Where(\{ _.Status -in ‘Failed’, ‘Skipped’ }).Count) {Write-Host “For Failed/Skipped tenants, a customer admin can approve with the FallbackConsentUrl in the CSV.” -ForegroundColor Yellow} Run the script: .\Grant-MspProcessSsoConsent.ps1 -WhatIf # dry run: shows which customers would be processed.\Grant-MspProcessSsoConsent.ps1 # all customers with active GDAP.\Grant-MspProcessSsoConsent.ps1 -TenantId <guid> # a single customer

You sign in to your own tenant first, to list your GDAP relationships, then to each customer tenant. Expect a sign-in or MFA prompt per customer.

What it does for each customer

  1. Creates the MSP Process SSO enterprise app, if it isn’t already there.
  2. Grants tenant-wide consent for openid profile email User.Read.
  3. Verifies the grant.
  4. Writes a CSV report named MspProcessSso-Consent-<timestamp>.csv .
It is safe to re-run. Existing apps and grants are reused.

Report statuses

Every Failed or Skipped row includes a FallbackConsentUrl. A customer admin can use it as in Option A.

Make a note of each customer’s tenant ID. You’ll find it in the script’s CSV report ( TenantId column), or in the tenant= value of the Option A redirect address. MSP Process uses it to match Microsoft sign-ins to the right customer. If you need help linking tenants to your customers, contact MSP Process Support.

Troubleshooting

Customer questions

What data does the app see?

Basic sign-in profile only: name, email address, and a tenant-unique user ID. It cannot read mail, files, or any other Microsoft 365 data.

Can the customer revoke it?

Yes. A customer admin can delete the enterprise app, or remove its permissions, in Entra admin center → Enterprise applications → MSP Process SSO.